Skip to content

Conversation

@matt-bernhardt
Copy link
Member

@matt-bernhardt matt-bernhardt commented Nov 10, 2025

This makes a pair of connected changes which focus on the process of switching tabs to see different sets of search results:

  1. The construction of the tabs in an initial page load are now handled by a helper function, which includes both CSS class names and an aria role being assigned. This class name and ARIA role are also updated by loading-spinner.js for updates after the initial page load.
  2. The interaction model for changing tabs has been updated, so that we add the "loading" spinner almost immediately upon page click, rather than as part of the turbo:frame-render event. We also immediately update the tabs' display as part of the same click handling sequence. The results panel's loading spinner is then removed when the new frame content renders.

This adds two tests for the helper function (but nothing for the javascript side of things).

I've tested this behavior in Firefox, Chrome, and Safari on a Mac, and using Firefox, Chrome, and Edge on a Windows 11 box. This sequence seems to work as desired on a visual basis. I've also checked the behavior with WAVE and ANDI, and listened to the interaction with VoiceOver. I've got some questions there which I'll share with Darcy for confirmation during our formal a11y testing.

Dave has confirmed that the behavior in the review app is what he was intending.

Ticket

How to see this behavior

The review app for this PR has all the changes, as well as the feature flag enabled that simulates slower API responses - so you've got a chance to see the spinner in action (thanks @JPrevost )

Side effects / notes:

The tests for this helper function should be robust enough, but I worry that they're a bit too clever. I didn't want to just write a string comparison assertion, because that feels very fragile given how much this partial has been changing lately. The assertions as written should focus on the values being present or not, but still be robust to other changes.

Also note that, for now, the source_tabs partial is still a bit bespoke in that there are explicitly two calls to the various tabs. I'd originally intended to define a list in the controller, but managing the list of tabs in this way feels awkward and like it doesn't gain anything since the tabs are entwined with so much of the rest of the application. Abstracting the tab order and contents to a list doesn't save us from other complexity.

Developer

Accessibility
  • ANDI or WAVE has been run in accordance to our guide.
  • This PR contains no changes to the view layer.
  • New issues flagged by ANDI or WAVE have been resolved.
  • New issues flagged by ANDI or WAVE have been ticketed (link in the Pull Request details above).
  • No new accessibility issues have been flagged.
New ENV
  • All new ENV is documented in README.
  • All new ENV has been added to Heroku Pipeline, Staging and Prod.
  • ENV has not changed.
Approval beyond code review
  • UXWS/stakeholder approval has been confirmed.
  • UXWS/stakeholder review will be completed retroactively.
  • UXWS/stakeholder review is not needed.
Additional context needed to review

E.g., if the PR includes updated dependencies and/or data
migration, or how to confirm the feature is working.

Code Reviewer

Code
  • I have confirmed that the code works as intended.
  • Any CodeClimate issues have been fixed or confirmed as
    added technical debt.
Documentation
  • The commit message is clear and follows our guidelines
    (not just this pull request message).
  • The documentation has been updated or is unnecessary.
  • New dependencies are appropriate or there were no changes.
Testing
  • There are appropriate tests covering any new functionality.
  • No additional test coverage is required.

@coveralls
Copy link

coveralls commented Nov 10, 2025

Pull Request Test Coverage Report for Build 19311046157

Warning: This coverage report may be inaccurate.

This pull request's base commit is no longer the HEAD commit of its target branch. This means it includes changes from outside the original pull request, including, potentially, unrelated coverage changes.

Details

  • 4 of 4 (100.0%) changed or added relevant lines in 1 file are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+0.01%) to 97.875%

Totals Coverage Status
Change from base Build 19302531738: 0.01%
Covered Lines: 875
Relevant Lines: 894

💛 - Coveralls

@mitlib mitlib temporarily deployed to timdex-ui-pi-use-180-tee54nbsx November 10, 2025 22:58 Inactive
** Why are these changes being introduced:

We want to add an `aria-current="page"` attribute to the currently
active tab link on a results page, so that assistive technologies can
parse which is the active tab. This requires a conditional link_to call,
as well as an update to the javascript which handles tab changes.

** Relevant ticket(s):

* https://mitlibraries.atlassian.net/browse/use-180

** How does this address that need:

Because we can't (apparently) write a conditional inside an argument to
a link_to call, we have to maintain the conditional check elsewhere -
so to prevent the source_tabs partial from becoming a mess of multiple
conditionas, we define a new search helper function, link_to_tab. This
takes a string as an argument, and is capable of outputting both a link
to the active tab, as well as to an inactive tab.

We also update loading_spinner.js to be able to maintain the state of
the aria-current attribute.

** Document any side effects to this change:

The tests for this helper function _should_ be robust enough, but I
worry that they're a bit too clever. I didn't want to just write a
string comparison assertion, because that feels very fragile given how
much this partial has been changing lately. The assertions as written
should focus on the values being present or not, but still be robust to
other changes.

Also note that, for now, the source_tabs partial is still a bit bespoke
in that there are explicitly two calls to the various tabs. I'd
originally intended to define a list in the controller, but managing the
list of tabs in this way feels awkward and like it doesn't gain anything
since the tabs are entwined with so much of the rest of the application.
Abstracting the tab order and contents to a list doesn't save us from
other complexity.
This moves the manipulation of the tabs to the click action (via a standalone function) rather than in the turbo:frame-render action.
Copy link
Member

@JPrevost JPrevost left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added comments to three changes that don't seem necessary. They work fine, but when I revert the CSS to what it was and remove the manual spinner class manipulation I literally see no difference locally. I'm curious if I'm missing something or if this is preferred for some reason?

currentTabLink.classList.add('active');
}
// Remove the spinner now that things are ready
document.getElementById('search-results').classList.remove('spinner');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the manual addition/removal adds complexity without improving the outcome in a meaningful way. Would you mind either clarifying why this is better or changing it back?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me test without this change - my thinking initially was the spinner wasn't appearing early enough - which seemed to be the case when I simulated low-bandwidth connections working locally.

I'll re-check now that everything is in place.

Copy link
Member Author

@matt-bernhardt matt-bernhardt Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I've set up a comparison to show the behavior I was noticing. This applies to two review apps:

  1. The review app for this PR, which has had the spinner CSS class reverted back to the prior state, and keys off commit b261749, can be seen at https://timdex-ui-pi-use-180-tee54nbsx.herokuapp.com/results?booleanType=AND&q=archives&tab=primo
  2. A comparison review app, which still has the spinner defined in a CSS class and added/removed manually. This app keys off commit f35d4ca, which just prior to the tip of this PR. This can be seen at https://timdex-ui-pi-use-180-or-ixjq7j.herokuapp.com/results?booleanType=AND&q=archives&tab=primo

Both apps use the "simulate search latency" feature, but there's another component to this - simulating slow network connections via the browser. The test sequence is:

  1. In your browser, set a network throttle via dev tools (Firefox allows me to go all the way down to 50 Kbps, although the behavior I can see emerges as a flicker at 4G speeds. It is quite pronounced at 2G speeds).
  2. Load a page of resuts, similar to the URL above.
  3. Flip back and forth between tabs - I think it might become more pronounced after doing this a few times, but I can't say that for sure. What you're looking for is that the spinner gets removed just before the tab content actually changes. When the browser throttle is particularly pronounced (i.e. the user has very slow internet connection), there can be a seconds-long gap between the spinner disappearing and the new tab content becoming visible.

I've found that once I've noticed this stutter, I find that I can notice it even on faster connections - but the stutter is minimal / non-existent if we remove the spinner manually as in the other review app.

I may be overthinking this - I like the approach of letting Turbo Frames control the spinner - but having seen that there are cases where the spinner doesn't get removed at the right moment, I worry about inducing confusion when a user starts to scroll after the spinner disappears, only to have a new batch of results appear after a moment.

const newTab = clickedParams.get('tab');

// Throw the spinner on the search results immediately
document.getElementById('search-results').classList.add('spinner');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the manual addition/removal adds complexity without improving the outcome in a meaningful way. Would you mind either clarifying why this is better or changing it back?


// Pagination overlay when loading
[busy]:not([no-spinner]) {
.spinner {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the manual addition/removal adds complexity without improving the outcome in a meaningful way. Would you mind either clarifying why this is better or changing it back?

@@ -1,3 +1,19 @@
// Update the tab UI to reflect the newly-requested state. This function is called
// by a click event handler in the tab UI. It follows a two-step process:
function swapTabs(new_target) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great call consolidating this logic into a function

end
end

def link_to_tab(target)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much cleaner in the view now and easier to test. ❤️

@mitlib mitlib temporarily deployed to timdex-ui-pi-use-180-tee54nbsx November 12, 2025 18:41 Inactive
@JPrevost
Copy link
Member

@matt-bernhardt I concur with your assessment with simulated low bandwidth devices. Please add your fix back in and we can move towards merge.

@mitlib mitlib temporarily deployed to timdex-ui-pi-use-180-tee54nbsx November 12, 2025 20:33 Inactive
@matt-bernhardt
Copy link
Member Author

@JPrevost I've restored the .spinner class and explicit handling

@matt-bernhardt matt-bernhardt merged commit 07a6180 into main Nov 13, 2025
5 checks passed
@matt-bernhardt matt-bernhardt deleted the use-180 branch November 13, 2025 14:38
@matt-bernhardt matt-bernhardt mentioned this pull request Nov 13, 2025
18 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants