From d07050aeb08ed423c1c3cecd1d58b6e82f066907 Mon Sep 17 00:00:00 2001 From: ARMAN S Date: Tue, 30 Jun 2026 17:29:54 +0530 Subject: [PATCH 1/4] feat(search): add count + loading state to OlSelectPopover Extends item shape from {value, label} to {value, label, count?}. Renders count as a muted, comma-formatted trailing number when present. Adds host-driven loading boolean attribute, swaps suggestions list for a spinner while true. No fetch or domain logic added - stays a pure UI component per the building-components architecture guide. Part of #13060 --- openlibrary/components/lit/OlSelectPopover.js | 53 ++++++++++++++++++- .../components/lit/custom-elements.json | 19 +++++++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/openlibrary/components/lit/OlSelectPopover.js b/openlibrary/components/lit/OlSelectPopover.js index c51115b03e2..56b68fc3285 100644 --- a/openlibrary/components/lit/OlSelectPopover.js +++ b/openlibrary/components/lit/OlSelectPopover.js @@ -84,6 +84,7 @@ export class OlSelectPopover extends FocusableHostMixin(LitElement) { clearLabel: { type: String, attribute: 'clear-label' }, noMatchesLabel: { type: String, attribute: 'no-matches-label' }, _query: { state: true }, + loading: { type: Boolean, reflect: true }, }; static styles = css` @@ -365,6 +366,45 @@ export class OlSelectPopover extends FocusableHostMixin(LitElement) { outline: var(--focus-width) solid var(--color-focus-ring); outline-offset: 2px; } + + /* ── Item count ──────────────────────────────────────────────── */ + + .item-count { + margin-left: auto; + flex-shrink: 0; + color: var(--accessible-grey); + font-size: var(--font-size-label-medium); + font-variant-numeric: tabular-nums; + } + + /* ── Host-driven loading state ───────────────────────────────────────── */ + + @keyframes ol-sp-spin { + to { transform: rotate(360deg); } + } + + .loading-row { + display: flex; + align-items: center; + justify-content: center; + gap: var(--spacing-inline-sm); + padding: var(--spacing-inset-md); + color: var(--accessible-grey); + font-size: var(--font-size-body-medium); + } + .loading-spinner { + width: 14px; + height: 14px; + border: 2px solid var(--color-border-subtle); + border-top-color: var(--accessible-grey); + border-radius: 50%; + flex-shrink: 0; + animation: ol-sp-spin 0.65s linear infinite; + } + + @media (prefers-reduced-motion: reduce) { + .loading-spinner { animation: none; opacity: 0.5; } + } `; /** Chevron icon for the default trigger */ @@ -396,6 +436,7 @@ export class OlSelectPopover extends FocusableHostMixin(LitElement) { // re-homes the item between the selected/suggestions groups (which // destroys its DOM node, so its focus is lost). this._restoreFocusToValue = null; + this.loading = false; } /** @@ -522,6 +563,13 @@ export class OlSelectPopover extends FocusableHostMixin(LitElement) { ` : nothing}
+ ${this.loading + ? html` +
+ + Loading… +
` + : html` + `}
${hasSelected ? html`