Move the search results toolbar onto web components - #13228
Conversation
Replaces the hand-rolled .tool-button--group anchors with the segmented control. Segments are icon-only; the `label` attribute supplies both the accessible name and the hover tooltip, which also sidesteps "Details"/"Grid" translating to very different widths. One template feeds four surfaces: search results, author pages, list view, and the reading log. The click handler already called preventDefault() unconditionally, so the anchors' hrefs were only a no-JS affordance for *changing* layout. Deep links still work: ?layout=grid is resolved server-side by get_remembered_layout(), independent of this control. Layout-switch analytics move from Athena to Matomo via trackEvent(), since the data-ol-link-track trigger can't see into a shadow root — the same move SearchFilterBar made for the availability toggle.
The two look alike but differ in what activating an item means: ol-options-popover is a form control — a radiogroup that holds a value, participates in forms, and moves its selection as you arrow through it (selection follows focus). ol-menu-popover is an action menu — role=menu with menuitemradio items, arrows move focus only, and Enter/Space/click activates and closes. Nothing is committed until the user acts, which is what makes it safe to wire to navigation. Not form-associated. Wiring a sort menu to the radiogroup would have fired a change per arrow keypress, so this exists rather than a restyled variant. Both compose ol-popover for the shell, so animation, focus trap, mobile tray, and Escape/outside-click dismissal are shared.
Replaces the <details>/<summary> sort dropper. The per-scheme option lists are unchanged; they're flattened into the component's items, each carrying the changequery URL the server built, so query preservation and page reset stay server-side. Sub-sorts now render as always-visible nested rows rather than appearing only once their parent is selected, which makes filters like "Want to Read" discoverable. The lead "Any" sub-sort is dropped: it duplicated its own parent's sort key. sort-dropper.css goes from 99 lines to the shuffle link — the panel, trigger, and selected state are all component-owned now.
Sort and layout join the availability and language filters on one row, pushed to the right; the count moves to its own row below and reads "1 - 20 of 518 results" instead of "518 hits". The filter row spans the full content width while the results column stops short of the facets sidebar, so the right-aligned tools would overhang the sidebar. Extracted --search-facets-width in the file that owns that layout; both the sidebar and the row's reserved space read from it, gated on the same 960px breakpoint where the sidebar moves alongside the results. Below 768px the tools take their own row rather than wrapping to the end of a half-empty line. num_found_exact is plumbed through from Solr's numFoundExact so the total can be qualified with "over" if OL ever enables minExactCount. It is always exact today, so the count is quoted as-is.
The off-state track sat only 6% below the hover fill, washing out into the button. --light-grey restores roughly the separation it has against the resting white.
New: "View as", "Sort by", "Sort results", and the result-range strings. Gone: "View as: " (trailing colon), "Sorting by". "%(count)s hits" stays — other pages still use it.
Cut the prose down to what isn't already obvious from the code. No behavior change; JSDoc tags that feed the design page's API tables are untouched.
Trigger reopen: _isOpen was set on open but cleared only on activation, so Escape or an outside click left it stale and ArrowDown stopped opening the menu after the first dismissal. Read ol-popover.open instead — the one thing that actually knows. heading="": documented as omitting the heading, but `||` sent an empty string through to the label-derived default. Default the property to null and use `??`. Slotted trigger: only the injected trigger got the "Sort by, Relevance" aria-label, so the sort control — which server-renders its own — read as a bare "Relevance" with no clue what it was a value of. Name either trigger, and never clobber one the consumer authored.
min(page * rows, num_found) assumes a full page, which is only sound while num_found is exact — the case num_found_exact was just plumbed through to detect. Counting the docs actually rendered is exact either way and can't disagree with the list below it.
The list isn't keyed — _renderPanel maps items with no repeat() — so duplicates wouldn't collide. Keeping the "Any" row would instead leave two rows reading as aria-checked.
A native title is slow to appear and invisible to touch. Wrap icon-only segments in ol-tooltip, placed below since these sit in toolbars where the space above belongs to another row. The wrapper is display:contents so the button stays a direct flex item of .layer--base and _measure()'s rects still line up with the ghost layer; firstUpdated waits on the tooltips' updateComplete so those buttons aren't measured at zero width.
The stats line labels the results below it, so it should sit closer to them than to the controls above. Drop the row's stack gap and add the difference back as margin-top on the stats line.
The pill was sized from a segment's border-box width but laid out as content-box, so its own border pushed it 2px past the last segment and ate the track's right padding. Its radius also matched the track's, which reads rounder than the corner containing it.
"Details" described the density, not the shape; "List" pairs with "Grid" as the thing you are actually choosing between.
There was a problem hiding this comment.
Niiiiice lgtm! Can't wait until we can replace the "Show ebooks only" with the readable toggle, too 😊 We tested search, authors, lists, and my books pages and it all worked reliably! No blockers.
UI/UX Feedback:
-
Sort option should turn blue when non-default
-
Not sure if this is because of genre explorer, which is also on testing and I believe made the top bar sticky, but sort menu is showing below search box
- Pre-existing issue: The word "Language" is getting clipped:
- Pre-existing issue: Browse/My Books are showing above the popover
Code feedback:
Hmm question for consideration. We use lit components for more pure UI primitives/atoms currently, which is great. But I wonder if we might want to also start having "compound" impure components that are more ol-specific and potentially contain business layer logic. Like ol-sort-menu or something, to replace the need for some of the html/macro level code. We do this already for ol-search-modal, which as a result doesn't live in the same directory as the primitive components, but which well-encapsulates network requests/etc as well as the rendering. This sort menu could potentially also be something like that, and that could simplify some of the code, and also keep relevant code close together -- whereas now it's split between the js and the macro. Food for thought!
| // Sort options popover (results toolbars) | ||
| const sortOptions = document.querySelector('.sort-options'); | ||
| if (sortOptions) { | ||
| import(/* webpackChunkName: "sort-options" */ './sort_options') |
There was a problem hiding this comment.
Note this will cause a separate network request for the sort options js; not sure if we want to bundle that instead with another js file. Not a blocker.
| /* The sort control is <ol-menu-popover>, painted by the component and | ||
| ol-button.css. Only the shuffle link beside it needs styling here. */ | ||
|
|
||
| .sort-random-shuffle { |
There was a problem hiding this comment.
Oh right! Tested and the shuffle button is still there/working. That's the only thing keeping this css file around.
Author pages and the reading log filtered availability through a "— Show only ebooks?" sentence link, which read as prose rather than a control and gave no way to see how much the filter would leave. Both now use the same ol-toggle the search page does, with sort and layout pushed to the far end of the row. The toggle stays visible while filtered even when nothing matches, so the filter is always reversible. Author pages carry a sublabel count from one rows=0 Solr query; the reading log doesn't, since counting a shelf's readable books means querying with every logged work id. results-filter-toggle.js is deliberately not SearchFilterBar: these filters only rewrite one URL param and remember nothing, so they stay scoped to the page they're set on.
When a search turns up author suggestions, the stats line sat above both groups while only counting one of them. With suggestions present the count now rides the "Books" label, next to the results it describes.
The searchbox now takes its border, radius, height, and text inset from the same tokens ol-button uses, so it reads as part of one control family with the filter and sort buttons beside it. The filter row gets a block break below it — it's the last control before the results — and the stats line starts on the result card's content edge.
internetarchive#13228 landed ol-menu-popover and documented it in the old Templetor design.html, which this branch replaced with the Jinja design system — so the docs come across as a COMPONENTS row plus a partial, and the orphaned Templetor page goes. The write-up is the upstream one, re-cut to the new shape: the menu-vs-options distinction leads (it's the reason to reach for one over the other), the three examples render live through ex.example() so the snippets can't drift, and keyboard/ARIA moves into the notes block. The API table needs nothing — it derives from the JSDoc. index.js keeps both init blocks; messages.pot is regenerated, which drops the reference to the deleted design/toggle.html.


Part of #13240 (Epic: UI Modernization — consistency, mobile, accessibility), under the Foundation workstream.
Modernizes the search results toolbar onto the web component library, and adds the menu component it needed.
Why now
The availability toggle and language filter on this row moved onto the web component library a few PRs back; sort and layout were left behind. The toolbar has been sitting half-migrated since — two visual languages and two keyboard models side by side — with sort still a
<details>dropdown full of bare links and layout a pair of<a>s standing in for a toggle: no menu or pressed-state semantics, no arrow-key navigation, and a desktop dropdown squeezed onto phones. This finishes the row, so the wider overhaul's wins — a11y, mobile, modern design — reach the last controls that were missing them.What changes for patrons
ol-segmented-controlwith icon-only segments (tooltips name them). One template, so this lands on search, author pages, list view, and the reading log.1 - 20 of 518 resultsinstead of518 hits.Technical
Adds
<ol-menu-popover>rather than restyling<ol-options-popover>. They look alike but differ in what activating an item means: options-popover is a form control (radiogroup, form-associated, selection follows focus as you arrow), menu-popover is an action menu (role=menu, arrows move focus only, Enter/click activates and closes). Wiring a sort menu to the radiogroup would have fired a change per arrow keypress — a page load per keystroke. Both composeol-popover, so focus trap, mobile tray, and dismissal are shared. Design page documents when to pick which.Other notes:
changequeryURL the server built, so query preservation and page reset stay server-side; the JS only navigates.trackEvent(). Same moveSearchFilterBaralready made.num_found_exactis plumbed through from Solr'snumFoundExactso the total could read "over N" if OL ever enablesminExactCount. It's always exact today.Testing
On
/search?q=ursula:LBLcookie).21 - 40 of 518, last page →501 - 518 of 518.Also worth a look, since they share the same templates: an author page (
?sort=excludes relevance), a list (lists_seedssorts), the reading log, and/languages.Screenshot
See above
Stakeholders
@mekarpeles @cdrini