Skip to content

Move the search results toolbar onto web components - #13228

Merged
lokesh merged 18 commits into
internetarchive:masterfrom
lokesh:feat/use-segmented-control
Aug 1, 2026
Merged

Move the search results toolbar onto web components#13228
lokesh merged 18 commits into
internetarchive:masterfrom
lokesh:feat/use-segmented-control

Conversation

@lokesh

@lokesh lokesh commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

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.

image

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

  • Details/Grid toggle is now an ol-segmented-control with icon-only segments (tooltips name them). One template, so this lands on search, author pages, list view, and the reading log.
  • Sort is now a popover menu instead of the dropper. Reading-log sub-sorts like "Want to Read" are always visible as nested rows instead of appearing only after you pick "Reading Log", so they're actually findable. The redundant "Any" row is gone.
  • Search toolbar regrouped: sort and layout join the availability and language filters on one row, right-aligned to the results column. The count moves below and reads 1 - 20 of 518 results instead of 518 hits.
  • Sort and layout are now the same size as the filters beside them, so the row reads as one set of controls.

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 compose ol-popover, so focus trap, mobile tray, and dismissal are shared. Design page documents when to pick which.

Other notes:

  • Sort items carry the changequery URL the server built, so query preservation and page reset stay server-side; the JS only navigates.
  • Layout/sort analytics move from Athena to Matomo via trackEvent(). Same move SearchFilterBar already made.
  • num_found_exact is plumbed through from Solr's numFoundExact so the total could read "over N" if OL ever enables minExactCount. It's always exact today.

Testing

On /search?q=ursula:

  1. Toggle Details/Grid — layout switches, choice persists across pages (LBL cookie).
  2. Open sort, press Down a few times — focus moves but nothing loads. Press Enter to apply. Escape after arrowing leaves the page alone and returns focus to the trigger.
  3. Check the range against paging: page 2 → 21 - 40 of 518, last page → 501 - 518 of 518.
  4. Narrow the window — under 960px the tools stop reserving sidebar space; under 768px they take their own row.

Also worth a look, since they share the same templates: an author page (?sort= excludes relevance), a list (lists_seeds sorts), the reading log, and /languages.

Screenshot

See above

Stakeholders

@mekarpeles @cdrini

lokesh added 15 commits July 28, 2026 23:11
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.
@lokesh
lokesh marked this pull request as ready for review July 29, 2026 22:48
@lokesh lokesh added the Needs: Review This issue/PR needs to be reviewed in order to be closed or merged (see comments). [managed] label Jul 29, 2026
@lokesh
lokesh requested a review from mekarpeles July 29, 2026 22:51

@cdrini cdrini left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Image
  • Pre-existing issue: The word "Language" is getting clipped:
Image
  • Pre-existing issue: Browse/My Books are showing above the popover
Image

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')

@cdrini cdrini Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Oh right! Tested and the shuffle button is still there/working. That's the only thing keeping this css file around.

@cdrini cdrini added Ready to Merge and removed Needs: Review This issue/PR needs to be reviewed in order to be closed or merged (see comments). [managed] labels Jul 31, 2026
lokesh added 3 commits July 31, 2026 17:01
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.
@lokesh

lokesh commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator Author
  1. Updated the 'Show only ebooks link' to use ol-toggle on the Author and List pages:
Screenshot 2026-07-31 at 9 41 54 PM
  1. For the scenario when search results show authors on the search results page, I've increased the spacing between the author and book result sections, and the results count for books has been moved down in proximity of the books section title:
Screenshot 2026-07-31 at 9 42 45 PM

@lokesh
lokesh merged commit 927a1c9 into internetarchive:master Aug 1, 2026
5 checks passed
lokesh added a commit to lokesh/openlibrary that referenced this pull request Aug 1, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants