From 52af1787ca71ac92000796e2a5a26dd4a9aca8fa Mon Sep 17 00:00:00 2001 From: jazairi <16103405+jazairi@users.noreply.github.com> Date: Tue, 4 Nov 2025 14:06:58 -0500 Subject: [PATCH] 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. --- app/assets/stylesheets/application.css.scss | 1 + .../partials/_loading_spinner.scss | 50 +++++++++++++++++++ app/assets/stylesheets/partials/_results.scss | 2 +- app/javascript/application.js | 3 +- app/javascript/loading_spinner.js | 23 +++++++++ app/views/search/results.html.erb | 2 +- config/importmap.rb | 1 + 7 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 app/assets/stylesheets/partials/_loading_spinner.scss create mode 100644 app/javascript/loading_spinner.js diff --git a/app/assets/stylesheets/application.css.scss b/app/assets/stylesheets/application.css.scss index 7d25d3f3..e59ed27c 100644 --- a/app/assets/stylesheets/application.css.scss +++ b/app/assets/stylesheets/application.css.scss @@ -18,5 +18,6 @@ @import "partials/_search"; @import "partials/_shared"; @import "partials/_results"; +@import "partials/_loading_spinner"; @import "partials/_typography"; @import "partials/_suggestion-panel"; diff --git a/app/assets/stylesheets/partials/_loading_spinner.scss b/app/assets/stylesheets/partials/_loading_spinner.scss new file mode 100644 index 00000000..8439a413 --- /dev/null +++ b/app/assets/stylesheets/partials/_loading_spinner.scss @@ -0,0 +1,50 @@ +// Loading indicator for pagination (Turbo Frame updates) +// Tab navigation uses full page loads with Turbo's built-in progress bar +// https://discuss.hotwired.dev/t/show-spinner-everytime-async-frame-reloads/3483/3 +@keyframes spinner { + to { + transform: rotate(360deg); + } +} + +// Pagination overlay when loading +[busy]:not([no-spinner]) { + position: relative; + min-height: 400px; + display: block; + + > * { + opacity: 0.3; + } + + // Loading text + &::before { + content: 'Loading results...'; + position: absolute; + top: 5.5rem; + left: 50%; + margin-left: -6rem; // Center the text box (12rem / 2) + width: 12rem; + font-size: $fs-small; + font-weight: $fw-semibold; + color: $color-black; + text-align: center; + z-index: 1001; + } + + // Spinner positioned above text + &::after { + content: ''; + position: absolute; + top: 1rem; + left: 50%; + margin-left: -2rem; + width: 3rem; + height: 3rem; + border-radius: 50%; + border: 0.3rem solid $color-gray-200; + border-top-color: $color-red-500; + animation: spinner 1s linear infinite; + z-index: 1000; + } +} diff --git a/app/assets/stylesheets/partials/_results.scss b/app/assets/stylesheets/partials/_results.scss index f7149296..047cffec 100644 --- a/app/assets/stylesheets/partials/_results.scss +++ b/app/assets/stylesheets/partials/_results.scss @@ -118,4 +118,4 @@ font-size: 1.8rem; line-height: 1.1; } -} +} \ No newline at end of file diff --git a/app/javascript/application.js b/app/javascript/application.js index 97bdd19e..b3e67a5a 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -1,6 +1,7 @@ // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails import "@hotwired/turbo-rails" import "controllers" +import "loading_spinner" // Show the progress bar after 200 milliseconds, not the default 500 -window.Turbo.setProgressBarDelay(200); +Turbo.config.drive.progressBarDelay = 200; \ No newline at end of file diff --git a/app/javascript/loading_spinner.js b/app/javascript/loading_spinner.js new file mode 100644 index 00000000..0e0873da --- /dev/null +++ b/app/javascript/loading_spinner.js @@ -0,0 +1,23 @@ +// Loading spinner behavior for pagination (Turbo Frame updates) +document.addEventListener('turbo:frame-render', function(event) { + if (window.pendingFocusAction === 'pagination') { + // Focus on first result for pagination + const firstResult = document.querySelector('.results-list .result h3 a, .results-list .result .record-title a'); + if (firstResult) { + firstResult.focus(); + } + // Clear the pending action + window.pendingFocusAction = null; + } +}); + +document.addEventListener('click', function(event) { + const clickedElement = event.target; + + // Handle pagination clicks + if (clickedElement.closest('.pagination-container') || + clickedElement.matches('.first a, .previous a, .next a')) { + window.scrollTo({ top: 0, behavior: 'smooth' }); + window.pendingFocusAction = 'pagination'; + } +}); \ No newline at end of file diff --git a/app/views/search/results.html.erb b/app/views/search/results.html.erb index 93e47ec0..aae8d9f9 100644 --- a/app/views/search/results.html.erb +++ b/app/views/search/results.html.erb @@ -14,7 +14,7 @@ <%= render(partial: 'trigger_tacos') if tacos_enabled? %> - <%= turbo_frame_tag "search-results" do %> + <%= turbo_frame_tag "search-results", data: { turbo_action: "advance" } do %>