Skip to content

Commit 52af178

Browse files
committed
Add loading spinner for Turbo Frame updates
Why these changes are being introduced: Users experience long delays when switching between pages due to slow Primo API responses, with no visual feedback indicating that the system is processing their request. Relevant ticket(s): - [USE-134](https://mitlibraries.atlassian.net/browse/USE-134) How this addresses that need: This adds a CSS spinner animation that appears during Turbo Frame updates. It uses Turbo's built-in `busy` attribute for reliable state detection. Turbo also adds `aria-busy` for screen reader users. Changing pages also now scrolls to the top of the page and refocuses on the first result of the next page. Side effects of this change: - Adds data-turbo-action="advance" to search results frame. - Spinner may briefly show during fast API responses. - Deprecated Turbo API syntax has been updated to current syntax. - This is different the from built-in Turbo progress bar that we use for page reloads, as that feature is not available for Turbo Frame updates. It's fairly straightforward to rebuild in JS if we decide we want consistent loading progress indicators.
1 parent b4195f1 commit 52af178

File tree

7 files changed

+79
-3
lines changed

7 files changed

+79
-3
lines changed

app/assets/stylesheets/application.css.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,6 @@
1818
@import "partials/_search";
1919
@import "partials/_shared";
2020
@import "partials/_results";
21+
@import "partials/_loading_spinner";
2122
@import "partials/_typography";
2223
@import "partials/_suggestion-panel";
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Loading indicator for pagination (Turbo Frame updates)
2+
// Tab navigation uses full page loads with Turbo's built-in progress bar
3+
// https://discuss.hotwired.dev/t/show-spinner-everytime-async-frame-reloads/3483/3
4+
@keyframes spinner {
5+
to {
6+
transform: rotate(360deg);
7+
}
8+
}
9+
10+
// Pagination overlay when loading
11+
[busy]:not([no-spinner]) {
12+
position: relative;
13+
min-height: 400px;
14+
display: block;
15+
16+
> * {
17+
opacity: 0.3;
18+
}
19+
20+
// Loading text
21+
&::before {
22+
content: 'Loading results...';
23+
position: absolute;
24+
top: 5.5rem;
25+
left: 50%;
26+
margin-left: -6rem; // Center the text box (12rem / 2)
27+
width: 12rem;
28+
font-size: $fs-small;
29+
font-weight: $fw-semibold;
30+
color: $color-black;
31+
text-align: center;
32+
z-index: 1001;
33+
}
34+
35+
// Spinner positioned above text
36+
&::after {
37+
content: '';
38+
position: absolute;
39+
top: 1rem;
40+
left: 50%;
41+
margin-left: -2rem;
42+
width: 3rem;
43+
height: 3rem;
44+
border-radius: 50%;
45+
border: 0.3rem solid $color-gray-200;
46+
border-top-color: $color-red-500;
47+
animation: spinner 1s linear infinite;
48+
z-index: 1000;
49+
}
50+
}

app/assets/stylesheets/partials/_results.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,4 +118,4 @@
118118
font-size: 1.8rem;
119119
line-height: 1.1;
120120
}
121-
}
121+
}

app/javascript/application.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
22
import "@hotwired/turbo-rails"
33
import "controllers"
4+
import "loading_spinner"
45

56
// Show the progress bar after 200 milliseconds, not the default 500
6-
window.Turbo.setProgressBarDelay(200);
7+
Turbo.config.drive.progressBarDelay = 200;

app/javascript/loading_spinner.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Loading spinner behavior for pagination (Turbo Frame updates)
2+
document.addEventListener('turbo:frame-render', function(event) {
3+
if (window.pendingFocusAction === 'pagination') {
4+
// Focus on first result for pagination
5+
const firstResult = document.querySelector('.results-list .result h3 a, .results-list .result .record-title a');
6+
if (firstResult) {
7+
firstResult.focus();
8+
}
9+
// Clear the pending action
10+
window.pendingFocusAction = null;
11+
}
12+
});
13+
14+
document.addEventListener('click', function(event) {
15+
const clickedElement = event.target;
16+
17+
// Handle pagination clicks
18+
if (clickedElement.closest('.pagination-container') ||
19+
clickedElement.matches('.first a, .previous a, .next a')) {
20+
window.scrollTo({ top: 0, behavior: 'smooth' });
21+
window.pendingFocusAction = 'pagination';
22+
}
23+
});

app/views/search/results.html.erb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
<%= render(partial: 'trigger_tacos') if tacos_enabled? %>
1616

17-
<%= turbo_frame_tag "search-results" do %>
17+
<%= turbo_frame_tag "search-results", data: { turbo_action: "advance" } do %>
1818
<div class="layout-3q1q layout-band top-space">
1919
<main id="results" class="col3q wrap-results">
2020
<% if @show_primo_continuation %>

config/importmap.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Pin npm packages by running ./bin/importmap
22

33
pin "application", preload: true
4+
pin "loading_spinner", preload: true
45
pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
56
pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true
67
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true

0 commit comments

Comments
 (0)