Skip to content

Commit 412908e

Browse files
committed
Overlay spinner on tabs and manage focus appropriately
1 parent 5aa2708 commit 412908e

File tree

5 files changed

+95
-20
lines changed

5 files changed

+95
-20
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: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,52 @@
1-
// Loading indicator for turbo frame updates
1+
// Loading indicator for Turbo Frame updates
22
// https://discuss.hotwired.dev/t/show-spinner-everytime-async-frame-reloads/3483/3
33
@keyframes spinner {
44
to {
55
transform: rotate(360deg);
66
}
77
}
88

9-
[busy]:not([no-spinner]) {
9+
// Show loading state in tab area
10+
.tab-navigation {
1011
position: relative;
12+
13+
// When busy, hide tabs and show spinner
14+
&.loading {
15+
.tab-link {
16+
visibility: hidden;
17+
}
1118

12-
> * {
13-
opacity: 0.5;
14-
}
19+
// Overlay to cover both tab areas
20+
&::after {
21+
content: '';
22+
position: absolute;
23+
top: 0;
24+
left: 0;
25+
width: 12rem;
26+
bottom: 0;
27+
background-color: $color-gray-100;
28+
border-radius: 0.25rem;
29+
z-index: 10;
30+
}
1531

16-
&::after {
17-
content: '';
18-
box-sizing: border-box;
19-
position: fixed;
20-
top: 50%;
21-
left: 50%;
22-
width: 2.5rem;
23-
height: 2.5rem;
24-
margin-top: -1.25rem;
25-
margin-left: -1.25rem;
26-
border-radius: 50%;
27-
border: 0.275rem solid rgba($color-red-500, 0.3);
28-
border-top-color: $color-red-500;
29-
animation: spinner 0.6s linear infinite;
30-
z-index: 9999;
32+
// Centered spinner within the overlay
33+
&::before {
34+
content: '';
35+
position: absolute;
36+
top: calc(50% - 0.75rem);
37+
left: calc(6rem - 0.75rem); // Center in 12rem wide overlay
38+
width: 1.5rem;
39+
height: 1.5rem;
40+
border-radius: 50%;
41+
border: 0.2rem solid rgba($color-red-500, 0.3);
42+
border-top-color: $color-red-500;
43+
animation: spinner 0.6s linear infinite;
44+
z-index: 11;
45+
}
3146
}
3247
}
48+
49+
// Dim results content when loading
50+
[busy]:not([no-spinner]) > * {
51+
opacity: 0.5;
52+
}

app/javascript/application.js

Lines changed: 1 addition & 0 deletions
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
67
Turbo.config.drive.progressBarDelay = 200;

app/javascript/loading_spinner.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Loading spinner behavior for search results
2+
document.addEventListener('turbo:frame-render', function(event) {
3+
// Remove loading state from tabs when frame finishes loading
4+
const tabs = document.querySelector('.tab-navigation');
5+
if (tabs) {
6+
tabs.classList.remove('loading');
7+
}
8+
9+
// Focus management after content loads
10+
if (window.pendingFocusAction) {
11+
if (window.pendingFocusAction === 'pagination') {
12+
// Focus on first result for pagination
13+
const firstResult = document.querySelector('.results-list .result h3 a, .results-list .result .record-title a');
14+
if (firstResult) {
15+
firstResult.focus();
16+
}
17+
} else if (window.pendingFocusAction === 'tab-switch') {
18+
// Focus on results summary for tab switches
19+
const resultsContext = document.querySelector('.results-context');
20+
if (resultsContext) {
21+
resultsContext.setAttribute('tabindex', '-1');
22+
resultsContext.focus();
23+
}
24+
}
25+
// Clear the pending action
26+
window.pendingFocusAction = null;
27+
}
28+
});
29+
30+
document.addEventListener('click', function(event) {
31+
const clickedElement = event.target;
32+
33+
// Handle pagination clicks (scroll to top and set pending focus action)
34+
if (clickedElement.matches('.tab-link')) {
35+
const tabs = document.querySelector('.tab-navigation');
36+
if (tabs) {
37+
tabs.classList.add('loading');
38+
}
39+
window.pendingFocusAction = 'tab-switch';
40+
}
41+
42+
// Handle pagination clicks (scroll to top and set pending focus action)
43+
if (clickedElement.closest('.pagination-container') ||
44+
clickedElement.matches('.first a, .previous a, .next a')) {
45+
const tabs = document.querySelector('.tab-navigation');
46+
if (tabs) {
47+
tabs.classList.add('loading');
48+
}
49+
window.scrollTo({ top: 0, behavior: 'smooth' });
50+
window.pendingFocusAction = 'pagination';
51+
}
52+
});

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)