Skip to content

Commit f907408

Browse files
authored
[772] fix: prevent content flash when using case studies filtering (#213)
- added loading and loaded CSS classes; - apply loading class while case studies are being fetched; - switch to loaded class once case studies are fully loaded;
1 parent 730bb8e commit f907408

7 files changed

Lines changed: 131 additions & 54 deletions

File tree

.cursor/rules/common

Submodule common updated 1 file

website/modules/asset/ui/src/index.js

Lines changed: 35 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,19 @@ import { setupTagSearchForInput } from './searchInputHandler';
1212
import { FilterModal } from './filterModal';
1313
import { initClientSideFiltering } from './clientSideFiltering';
1414

15-
/* eslint-enable sort-imports */
15+
function revealLoaded() {
16+
document
17+
.querySelectorAll(
18+
'.breadcrumb.loading, .sf-container.loading, .page-main_content.loading',
19+
)
20+
.forEach((el) => {
21+
el.classList.remove('loading');
22+
el.classList.add('loaded');
23+
if (el.hasAttribute('aria-busy')) {
24+
el.setAttribute('aria-busy', 'false');
25+
}
26+
});
27+
}
1628

1729
function initConfiguration() {
1830
window.DEFAULT_VISIBLE_TAGS_COUNT = 5;
@@ -57,13 +69,11 @@ function initFontChanger() {
5769

5870
setInterval(() => {
5971
currentFontIndex = (currentFontIndex + 1) % fonts.length;
60-
// Use Array.prototype.at() for safer array access
6172
const currentFont = fonts.at(currentFontIndex);
6273
heroContent.style.fontFamily = currentFont;
6374
}, 500);
6475
}
6576

66-
// Tag search filter for case studies page
6777
function initCaseStudiesTagFilter({
6878
inputSelector = '.tag-search',
6979
containerSelector = '.filter-section',
@@ -92,30 +102,24 @@ function initializeAllComponents() {
92102
initClientSideFiltering();
93103
}
94104

95-
// Barba pages
96105
function initBarbaPageTransitions() {
97106
if (!document.querySelector('[data-barba="container"]')) return;
98107

99108
apos.util.onReady(() => {
100-
// Initialize case studies filter handler
101109
initCaseStudiesFilterHandler();
102110

103-
// Original Barba enter callback
104111
const originalEnterCallback = function (data, hasFilterAnchor) {
105-
// Scroll to the top after page transition (unless we have a filter anchor)
106112
if (!hasFilterAnchor) {
107113
window.scrollTo(0, 0);
108114
}
109115

110-
// Close menu if it's open
111116
const menuButton = document.getElementById('nav-icon');
112117
const menu = document.querySelector('[data-menu]');
113118
if (menuButton && menu) {
114119
menu.classList.remove('open');
115120
menuButton.classList.remove('open');
116121
}
117122

118-
// Trigger video play after transition
119123
const video = data.next.container.querySelector('video');
120124
if (video) {
121125
video.play();
@@ -160,7 +164,6 @@ function initBarbaPageTransitions() {
160164
cacheIgnore: false,
161165
preventRunning: true,
162166
timeout: 10000,
163-
164167
transitions: [
165168
{
166169
sync: false,
@@ -175,28 +178,27 @@ function initBarbaPageTransitions() {
175178
],
176179
});
177180

178-
// Add after hook for updating menu state
179181
barba.hooks.after(() => {
180182
// Update menu active state
181183
const currentPath = window.location.pathname;
182184
const menuLinks = document.querySelectorAll('.sf-nav__list a');
183-
184-
// First, remove active class from all menu items
185185
menuLinks.forEach((link) => link.classList.remove('active'));
186-
187-
// Then, add active class to the current menu item
188186
menuLinks.forEach((link) => {
189187
const href = link.getAttribute('href');
190188
const hrefPath = new URL(href, window.location.origin).pathname;
191189
if (hrefPath === currentPath) {
192190
link.classList.add('active');
193191
}
194192
});
193+
194+
revealLoaded();
195+
if (!window.caseStudiesFilterModal) {
196+
initFilterModal();
197+
}
195198
});
196199
});
197200
}
198201

199-
// Anchor Navigation
200202
function initAnchorNavigation() {
201203
const anchors = document.querySelectorAll('a[href^="#"]');
202204
if (!anchors.length) return;
@@ -216,7 +218,6 @@ function initAnchorNavigation() {
216218

217219
function initMenuToggle() {
218220
apos.util.onReady(() => {
219-
// Menu Open
220221
const menuButton = document.getElementById('nav-icon');
221222
const menu = document.querySelector('[data-menu]');
222223

@@ -227,7 +228,6 @@ function initMenuToggle() {
227228
menuButton.classList.toggle('open');
228229
});
229230

230-
// Close menu when clicking on menu items for non-logged users
231231
const menuLinks = menu.querySelectorAll('a');
232232
menuLinks.forEach((link) => {
233233
link.addEventListener('click', () => {
@@ -240,7 +240,6 @@ function initMenuToggle() {
240240
});
241241
}
242242

243-
// Filter Case Studies modal for Case Studies mobile page
244243
function initFilterModal() {
245244
if (!document.querySelector('.cs_list')) {
246245
return;
@@ -257,19 +256,23 @@ function initFilterModal() {
257256
});
258257
}
259258

260-
document.addEventListener('DOMContentLoaded', initFilterModal);
261-
262-
if (typeof barba !== 'undefined') {
263-
barba.hooks.after(() => {
264-
initFilterModal();
265-
});
259+
if (document.readyState === 'loading') {
260+
document.addEventListener('DOMContentLoaded', initFilterModal);
261+
} else {
262+
initFilterModal();
266263
}
267264

268265
export default () => {
269266
initConfiguration();
270267

271268
initializeAllComponents();
272269

270+
if (document.readyState === 'loading') {
271+
document.addEventListener('DOMContentLoaded', revealLoaded);
272+
} else {
273+
revealLoaded();
274+
}
275+
273276
apos.util.onReady(() => {
274277
initCaseStudiesFilterHandler();
275278
});
@@ -278,17 +281,15 @@ export default () => {
278281
initAnchorNavigation();
279282
initMenuToggle();
280283

281-
// Case studies anchor fix
282284
setTimeout(() => {
283285
const { pathname, search, hash } = window.location;
284-
const isCasesPage = pathname.includes('/cases');
285-
const hasFilterParams =
286-
search.includes('industry') ||
287-
search.includes('stack') ||
288-
search.includes('caseStudyType') ||
289-
hash.includes('filter');
290-
291-
if (isCasesPage && hasFilterParams) {
286+
if (
287+
pathname.includes('/cases') &&
288+
(search.includes('industry') ||
289+
search.includes('stack') ||
290+
search.includes('caseStudyType') ||
291+
hash.includes('filter'))
292+
) {
292293
const filterAnchor = document.getElementById('filter');
293294
if (filterAnchor) filterAnchor.scrollIntoView({ behavior: 'smooth' });
294295
}

website/modules/asset/ui/src/scss/_animation.scss

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,40 @@
5555
filter: blur(0);
5656
}
5757
}
58+
59+
// Loading/Loaded states for reveal animations
60+
.loading {
61+
opacity: 0;
62+
overflow: hidden;
63+
visibility: hidden;
64+
transform: translateY(20px);
65+
transition:
66+
opacity 0.6s ease-out,
67+
transform 0.6s ease-out;
68+
}
69+
70+
.loaded {
71+
opacity: 1;
72+
overflow: visible;
73+
visibility: visible;
74+
transform: translateY(0);
75+
}
76+
77+
// Specific loading states for breadcrumbs and containers
78+
.breadcrumb.loading,
79+
.sf-container.loading {
80+
opacity: 0 !important;
81+
visibility: hidden !important;
82+
transform: translateY(15px);
83+
transition:
84+
opacity 0.5s ease-out,
85+
visibility 0.5s ease-out,
86+
transform 0.5s ease-out;
87+
}
88+
89+
.breadcrumb.loaded,
90+
.sf-container.loaded {
91+
opacity: 1 !important;
92+
visibility: visible !important;
93+
transform: translateY(0);
94+
}

website/modules/asset/ui/src/scss/_cases.scss

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
&_content {
88
display: flex;
99
flex-direction: column;
10-
gap: 2rem;
1110
margin-bottom: 3rem;
1211
}
1312

website/modules/case-studies-page/views/index.html

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@
66
data-default-visible-tags="{{ data.defaultVisibleTagsCount }}"
77
>
88
<div class="cs_content">
9-
<div class="page-main_content">{% area data.page, 'main' %}</div>
9+
<div
10+
class="page-main_content loading"
11+
id="page-main_content"
12+
aria-busy="true"
13+
>
14+
{% area data.page, 'main' %}
15+
</div>
1016

1117
<div id="filter" class="filter-anchor"></div>
1218

@@ -295,6 +301,16 @@ <h4 class="portfolio-title">{{ article.portfolioTitle }}</h4>
295301
<script>
296302
window.totalPages = {{ data.totalPages | json }};
297303
</script>
304+
305+
<noscript>
306+
<style>
307+
#page-main_content {
308+
opacity: 1 !important;
309+
overflow: visible !important;
310+
visibility: visible !important;
311+
}
312+
</style>
313+
</noscript>
298314
<script src="/js/modules/case-studies-page/infinite-scroll.js"></script>
299315

300316
{% endblock main %}

website/views/fragments/fragments.html

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,37 +31,52 @@
3131
<footer class="sf-footer sf-container">
3232
<div class="sf-footer__content">
3333
<div class="sf-footer__top">
34-
<p class="sf-footer__tagline">A Deliberately Developmental Organization.</p>
34+
<p class="sf-footer__tagline">
35+
A Deliberately Developmental Organization.
36+
</p>
3537
<p class="sf-footer__tagline">Built by real people, for real impact.</p>
36-
<p class="sf-footer__tagline">Let's grow something meaningful together.</p>
38+
<p class="sf-footer__tagline">
39+
Let's grow something meaningful together.
40+
</p>
3741
</div>
38-
42+
3943
<div class="sf-footer__links">
4044
<div class="sf-footer__links-group">
41-
<a href="https://www.linkedin.com/company/speed-and-function/"
42-
class="sf-footer__links-item"
43-
target="_blank"
44-
rel="noopener noreferrer">
45+
<a
46+
href="https://www.linkedin.com/company/speed-and-function/"
47+
class="sf-footer__links-item"
48+
target="_blank"
49+
rel="noopener noreferrer"
50+
>
4551
linkedin
4652
</a>
47-
<a href="mailto:connect@speedandfunction.com"
48-
class="sf-footer__links-item">
53+
<a
54+
href="mailto:connect@speedandfunction.com"
55+
class="sf-footer__links-item"
56+
>
4957
connect@speedandfunction.com
5058
</a>
5159
</div>
52-
<a href="/privacy-policy" class="sf-footer__links-item sf-footer__links-item--policy">
60+
<a
61+
href="/privacy-policy"
62+
class="sf-footer__links-item sf-footer__links-item--policy"
63+
>
5364
privacy policy
5465
</a>
5566
</div>
56-
67+
5768
<div class="sf-footer__bottom">
5869
<p class="sf-copy">&#169; {{ data.currentYear or '2025' }} S&amp;F</p>
5970
</div>
6071
</div>
6172
</footer>
6273
{% endfragment %} {% fragment breadcrumbs() %} {% if data.page._url !=
6374
data.home._url %}
64-
<nav class="breadcrumb" aria-label="Breadcrumb navigation">
75+
<nav
76+
class="breadcrumb loading"
77+
aria-label="Breadcrumb navigation"
78+
aria-busy="true"
79+
>
6580
<ol class="breadcrumb-list">
6681
{% for page in data.page._ancestors %}
6782
<li class="breadcrumb-item">

website/views/layout.html

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,19 @@
2323
<div
2424
class="bp-wrapper relative"
2525
data-barba="wrapper"
26-
data-barba-namespace="home"
2726
>
2827
{% render fragments.header() %}
2928
<main
3029
class="bp-main"
3130
role="main"
3231
id="main"
3332
{% if not data.user %}
34-
data-barba="container"
33+
data-barba="container"
34+
data-barba-namespace="{{ (data.page and data.page.type) or 'default' }}"
3535
{% endif %}
3636
>
3737
{% render fragments.breadcrumbs() %}
38-
<div class="sf-container">
38+
<div class="sf-container loading" aria-busy="true" data-reveal="dom">
3939
{% endblock %}
4040

4141
{% block main %}
@@ -46,7 +46,16 @@
4646
</div>
4747
</main>
4848
{% render fragments.footer() %}
49-
<script type="text/javascript"></script>
49+
<noscript>
50+
<style>
51+
.breadcrumb.loading,
52+
.sf-container.loading {
53+
opacity: 1 !important;
54+
overflow: visible !important;
55+
visibility: visible !important;
56+
}
57+
</style>
58+
</noscript>
5059
</div>
51-
{# Close .bp-wrapper #}
60+
{# Close .bp-wrapper #}
5261
{% endblock %}

0 commit comments

Comments
 (0)