From 69c9422d3b2116a43ed59dfaf86da90446e7083d Mon Sep 17 00:00:00 2001 From: Artyom Savchenko Date: Fri, 24 Apr 2026 11:55:39 +0700 Subject: [PATCH] Add found elements count in desktop (#10799) Signed-off-by: Artem Savchenko --- desktop/src/main/findInPage.ts | 2 +- desktop/src/ui/findInPageBar.ts | 60 ++++++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/desktop/src/main/findInPage.ts b/desktop/src/main/findInPage.ts index eb9cb844d0..354fc327ae 100644 --- a/desktop/src/main/findInPage.ts +++ b/desktop/src/main/findInPage.ts @@ -27,7 +27,7 @@ const pagesWithFoundInPageListener = new WeakSet() const overlayViewsByWindowId = new Map() const overlayVisibleByWindowId = new Map() -const OVERLAY_WIDTH = 392 +const OVERLAY_WIDTH = 432 const OVERLAY_HEIGHT = 64 function resolveFindTarget (sender: WebContents): WebContents { diff --git a/desktop/src/ui/findInPageBar.ts b/desktop/src/ui/findInPageBar.ts index f5eb706e7f..b25d33358f 100644 --- a/desktop/src/ui/findInPageBar.ts +++ b/desktop/src/ui/findInPageBar.ts @@ -20,7 +20,8 @@ const DEBOUNCE_MS = 300 /** * Find UI is loaded in a separate BrowserView (see `findInPageOverlayHost.ts`) so * `findInPage` on the page webContents does not search the query in this input. - * Shadow DOM keeps chrome text minimal. Match counts are not shown. + * Shadow DOM keeps chrome text minimal. Match count / current index come from + * `found-in-page` forwarded as `onFindInPageResult`. * * Chromium moves focus to the matched text in the **page** webContents after * `findInPage`, so the overlay stops receiving keystrokes unless we put focus back @@ -49,6 +50,11 @@ export function setupDesktopFindInPageBar (electronApi: IPCMainExposed): void { input.spellcheck = false input.setAttribute('aria-label', 'Find in page') + const matchCount = document.createElement('span') + matchCount.className = 'match-count' + matchCount.setAttribute('aria-live', 'polite') + matchCount.setAttribute('aria-atomic', 'true') + const nav = document.createElement('div') nav.className = 'nav' nav.setAttribute('role', 'group') @@ -76,6 +82,7 @@ export function setupDesktopFindInPageBar (electronApi: IPCMainExposed): void { closeBtn.title = 'Close' root.appendChild(input) + root.appendChild(matchCount) root.appendChild(nav) root.appendChild(closeBtn) shadow.appendChild(style) @@ -89,6 +96,19 @@ export function setupDesktopFindInPageBar (electronApi: IPCMainExposed): void { return input.value.trim() } + function clearMatchCount (): void { + matchCount.textContent = '' + matchCount.removeAttribute('title') + } + + function describeMatchCount (matches: number, activeOrdinal: number): { label: string, title: string } { + if (matches <= 0) { + return { label: '0/0', title: 'No matches' } + } + const cur = activeOrdinal > 0 ? Math.min(activeOrdinal, matches) : 1 + return { label: `${cur}/${matches}`, title: `Match ${cur} of ${matches}` } + } + function updateNavState (): void { const q = getQuery() prevBtn.disabled = q === '' @@ -114,6 +134,7 @@ export function setupDesktopFindInPageBar (electronApi: IPCMainExposed): void { function hide (): void { visible = false host.style.display = 'none' + clearMatchCount() prevBtn.disabled = true nextBtn.disabled = true try { @@ -165,6 +186,7 @@ export function setupDesktopFindInPageBar (electronApi: IPCMainExposed): void { debounceTimer = undefined const q = getQuery() if (q === '') { + clearMatchCount() void electronApi.stopFindInPage('clearSelection').catch(() => {}) updateNavState() return @@ -226,6 +248,20 @@ export function setupDesktopFindInPageBar (electronApi: IPCMainExposed): void { }, true ) + + electronApi.onFindInPageResult((result) => { + if (!visible) { + return + } + const q = getQuery() + if (q === '') { + clearMatchCount() + return + } + const { label, title } = describeMatchCount(result.matches, result.activeMatchOrdinal) + matchCount.textContent = label + matchCount.title = title + }) } /** Left-pointing chevron; `.next::after` mirrors with `scaleX(-1)` for identical vertical alignment. */ @@ -314,6 +350,28 @@ const shadowStyles = ` color: transparent; } +.match-count { + min-width: 1.5rem; + padding: 0 2px; + font-size: 12px; + line-height: 1.35; + font-variant-numeric: tabular-nums; + text-align: right; + color: rgba(0, 0, 0, 0.52); + user-select: none; + flex-shrink: 0; +} + +@media (prefers-color-scheme: dark) { + .match-count { + color: rgba(255, 255, 255, 0.48); + } +} + +:host-context([data-theme='theme-dark']) .match-count { + color: rgba(255, 255, 255, 0.48); +} + .nav { display: flex; align-items: center;