Repository: discourse/discourse
Pull request #32099: FIX: Consistent search shortcuts
Author: login: martin-brennan
Created: 2025-04-01T06:11:50Z | Merged: 2025-04-04T01:54:46Z

We now have 3 search UX variations:

* Header search
* Welcome banner search
* Icon search

And within each of these there is a `<SearchMenu />`
component with an input that needs to be focused based
on either Ctrl+F or `/` shortcuts. This commit makes
sure that each has a unique ID, and moves the functionality
of determining the "current" input ID and focusing the
input to the search service.

This fixes issues like where pressing Ctrl+F twice on
the header search would not reveal the regular browser
search.



Diff:
diff --git a/app/assets/javascripts/discourse/app/components/header.gjs b/app/assets/javascripts/discourse/app/components/header.gjs
index d69a899ce1ec0..f0d67021bb83a 100644
--- a/app/assets/javascripts/discourse/app/components/header.gjs
+++ b/app/assets/javascripts/discourse/app/components/header.gjs
@@ -295,6 +295,7 @@ export default class GlimmerHeader extends Component {
             <SearchMenuWrapper
               @closeSearchMenu={{this.toggleSearchMenu}}
               {{this.handleFocus}}
+              @searchInputId="icon-search-input"
             />
           {{else if this.header.hamburgerVisible}}
             <HamburgerDropdownWrapper
diff --git a/app/assets/javascripts/discourse/app/components/header/contents.gjs b/app/assets/javascripts/discourse/app/components/header/contents.gjs
index 953678461a442..8ae9a3083579c 100644
--- a/app/assets/javascripts/discourse/app/components/header/contents.gjs
+++ b/app/assets/javascripts/discourse/app/components/header/contents.gjs
@@ -18,6 +18,7 @@ export default class Contents extends Component {
   @service header;
   @service router;
   @service sidebarState;
+  @service search;
 
   get sidebarIcon() {
     if (this.sidebarState.adminSidebarAllowedWithLegacyNavigationMenu) {
@@ -47,12 +48,10 @@ export default class Contents extends Component {
       return false;
     }
 
-    const searchExperience = applyValueTransformer(
-      "site-setting-search-experience",
-      this.siteSettings.search_experience
+    return (
+      this.search.searchExperience === "search_field" &&
+      !this.args.topicInfoVisible
     );
-
-    return searchExperience === "search_field" && !this.args.topicInfoVisible;
   }
 
   <template>
diff --git a/app/assets/javascripts/discourse/app/components/header/header-search.gjs b/app/assets/javascripts/discourse/app/components/header/header-search.gjs
index 8f7c35baa7d9a..7b0ff81639dfb 100644
--- a/app/assets/javascripts/discourse/app/components/header/header-search.gjs
+++ b/app/assets/javascripts/discourse/app/components/header/header-search.gjs
@@ -2,7 +2,7 @@ import Component from "@glimmer/component";
 import { service } from "@ember/service";
 import { modifier } from "ember-modifier";
 import DButton from "discourse/components/d-button";
-import SearchMenu, { focusSearchInput } from "discourse/components/search-menu";
+import SearchMenu from "discourse/components/search-menu";
 import bodyClass from "discourse/helpers/body-class";
 import concatClass from "discourse/helpers/concat-class";
 
@@ -11,13 +11,14 @@ export default class HeaderSearch extends Component {
   @service siteSettings;
   @service currentUser;
   @service appEvents;
+  @service search;
 
   advancedSearchButtonHref = "/search?expanded=true";
 
   handleKeyboardShortcut = modifier(() => {
     const cb = (appEvent) => {
       if (appEvent.type === "search" || appEvent.type === "page-search") {
-        focusSearchInput();
+        this.search.focusSearchInput();
         appEvent.event.preventDefault();
       }
     };
@@ -51,7 +52,10 @@ export default class HeaderSearch extends Component {
                   @href={{this.advancedSearchButtonHref}}
                 />
 
-                <SearchMenu @location="header" />
+                <SearchMenu
+                  @location="header"
+                  @searchInputId="header-search-input"
+                />
               </div>
             </div>
           </div>
diff --git a/app/assets/javascripts/discourse/app/components/header/icons.gjs b/app/assets/javascripts/discourse/app/components/header/icons.gjs
index 236fea6d68583..c2385efb8a559 100644
--- a/app/assets/javascripts/discourse/app/components/header/icons.gjs
+++ b/app/assets/javascripts/discourse/app/components/header/icons.gjs
@@ -5,7 +5,6 @@ import { eq } from "truth-helpers";
 import InterfaceColorSelector from "discourse/components/interface-color-selector";
 import DAG from "discourse/lib/dag";
 import getURL from "discourse/lib/get-url";
-import { applyValueTransformer } from "discourse/lib/transformer";
 import Dropdown from "./dropdown";
 import UserDropdown from "./user-dropdown";
 
@@ -60,14 +59,9 @@ export default class Icons extends Component {
       return false;
     }
 
-    const searchExperience = applyValueTransformer(
-      "site-setting-search-experience",
-      this.siteSettings.search_experience
-    );
-
     return (
       this.site.mobileView ||
-      searchExperience === "search_icon" ||
+      this.search.searchExperience === "search_icon" ||
       this.args.topicInfoVisible
     );
   }
diff --git a/app/assets/javascripts/discourse/app/components/header/search-menu-wrapper.gjs b/app/assets/javascripts/discourse/app/components/header/search-menu-wrapper.gjs
index 1f3da343d0991..925a96529f4d4 100644
--- a/app/assets/javascripts/discourse/app/components/header/search-menu-wrapper.gjs
+++ b/app/assets/javascripts/discourse/app/components/header/search-menu-wrapper.gjs
@@ -2,7 +2,10 @@ import SearchMenuPanel from "../search-menu-panel";
 
 const SearchMenuWrapper = <template>
   <div class="search-menu glimmer-search-menu" aria-live="polite" ...attributes>
-    <SearchMenuPanel @closeSearchMenu={{@closeSearchMenu}} />
+    <SearchMenuPanel
+      @searchInputId={{@searchInputId}}
+      @closeSearchMenu={{@closeSearchMenu}}
+    />
   </div>
 </template>;
 
diff --git a/app/assets/javascripts/discourse/app/components/search-menu-panel.gjs b/app/assets/javascripts/discourse/app/components/search-menu-panel.gjs
index 865ea375d70e6..28a2fa2c02493 100644
--- a/app/assets/javascripts/discourse/app/components/search-menu-panel.gjs
+++ b/app/assets/javascripts/discourse/app/components/search-menu-panel.gjs
@@ -22,6 +22,7 @@ export default class SearchMenuPanel extends Component {
         @inlineResults={{true}}
         @autofocusInput={{true}}
         @location="header"
+        @searchInputId={{@searchInputId}}
       />
     </MenuPanel>
   </template>
diff --git a/app/assets/javascripts/discourse/app/components/search-menu.gjs b/app/assets/javascripts/discourse/app/components/search-menu.gjs
index b23991015b13a..eb37fa0284609 100644
--- a/app/assets/javascripts/discourse/app/components/search-menu.gjs
+++ b/app/assets/javascripts/discourse/app/components/search-menu.gjs
@@ -33,14 +33,10 @@ import userSearch from "discourse/lib/user-search";
 const CATEGORY_SLUG_REGEXP = /(\#[a-zA-Z0-9\-:]*)$/gi;
 const USERNAME_REGEXP = /(\@[a-zA-Z0-9\-\_]*)$/gi;
 const SUGGESTIONS_REGEXP = /(in:|status:|order:|:)([a-zA-Z]*)$/gi;
-export const SEARCH_INPUT_ID = "search-term";
+export const SEARCH_INPUT_ID = "icon-search-input";
 export const MODIFIER_REGEXP = /.*(\#|\@|:).*$/gi;
 export const DEFAULT_TYPE_FILTER = "exclude_topics";
 
-export function focusSearchInput(inputId = SEARCH_INPUT_ID) {
-  document.getElementById(inputId).focus();
-}
-
 export default class SearchMenu extends Component {
   @service search;
   @service currentUser;
@@ -189,7 +185,7 @@ export default class SearchMenu extends Component {
     e.stopPropagation();
     e.preventDefault();
     this.search.activeGlobalSearchTerm = "";
-    focusSearchInput();
+    this.search.focusSearchInput();
     this.triggerSearch();
   }
 
@@ -452,6 +448,7 @@ export default class SearchMenu extends Component {
 
       {{#if @inlineResults}}
         <Results
+          @searchInputId={{@searchInputId}}
           @loading={{this.loading}}
           @invalidTerm={{this.invalidTerm}}
           @suggestionKeyword={{this.suggestionKeyword}}
@@ -467,6 +464,7 @@ export default class SearchMenu extends Component {
       {{else if this.displayMenuPanelResults}}
         <MenuPanel @panelClass="search-menu-panel">
           <Results
+            @searchInputId={{@searchInputId}}
             @loading={{this.loading}}
             @invalidTerm={{this.invalidTerm}}
             @suggestionKeyword={{this.suggestionKeyword}}
diff --git a/app/assets/javascripts/discourse/app/components/search-menu/results.gjs b/app/assets/javascripts/discourse/app/components/search-menu/results.gjs
index 49fc1e7434b67..2233f2cc87941 100644
--- a/app/assets/javascripts/discourse/app/components/search-menu/results.gjs
+++ b/app/assets/javascripts/discourse/app/components/search-menu/results.gjs
@@ -93,6 +93,7 @@ export default class Results extends Component {
             <div class="no-results">{{i18n "search.no_results"}}</div>
           {{else if this.renderInitialOptions}}
             <InitialOptions
+              @searchInputId={{@searchInputId}}
               @closeSearchMenu={{@closeSearchMenu}}
               @searchTermChanged={{@searchTermChanged}}
             />
@@ -100,6 +101,7 @@ export default class Results extends Component {
             {{#if (and (not @searchTopics) (not @inPMInboxContext))}}
               {{! render the first couple suggestions before a search has been performed}}
               <InitialOptions
+                @searchInputId={{@searchInputId}}
                 @closeSearchMenu={{@closeSearchMenu}}
                 @searchTermChanged={{@searchTermChanged}}
               />
diff --git a/app/assets/javascripts/discourse/app/components/search-menu/results/assistant-item.gjs b/app/assets/javascripts/discourse/app/components/search-menu/results/assistant-item.gjs
index cae33e3c619df..567407033d97d 100644
--- a/app/assets/javascripts/discourse/app/components/search-menu/results/assistant-item.gjs
+++ b/app/assets/javascripts/discourse/app/components/search-menu/results/assistant-item.gjs
@@ -3,7 +3,6 @@ import { on } from "@ember/modifier";
 import { action } from "@ember/object";
 import { service } from "@ember/service";
 import { and, or } from "truth-helpers";
-import { focusSearchInput } from "discourse/components/search-menu";
 import Category from "discourse/components/search-menu/results/type/category";
 import Tag from "discourse/components/search-menu/results/type/tag";
 import User from "discourse/components/search-menu/results/type/user";
@@ -128,7 +127,7 @@ export default class AssistantItem extends Component {
       ...(inTopicContext &&
         !this.args.searchAllTopics && { setTopicContext: true }),
     });
-    focusSearchInput();
+    this.search.focusSearchInput();
   }
 
   <template>
diff --git a/app/assets/javascripts/discourse/app/components/search-menu/results/initial-options.gjs b/app/assets/javascripts/discourse/app/components/search-menu/results/initial-options.gjs
index a762e9d7bef99..eaa9ca8f3261b 100644
--- a/app/assets/javascripts/discourse/app/components/search-menu/results/initial-options.gjs
+++ b/app/assets/javascripts/discourse/app/components/search-menu/results/initial-options.gjs
@@ -213,7 +213,10 @@ export default class InitialOptions extends Component {
               {{/if}}
             {{/if}}
           {{else}}
-            <RandomQuickTip @searchTermChanged={{@searchTermChanged}} />
+            <RandomQuickTip
+              @searchInputId={{@searchInputId}}
+              @searchTermChanged={{@searchTermChanged}}
+            />
             {{#if (and this.currentUser this.siteSettings.log_search_queries)}}
               <RecentSearches
                 @closeSearchMenu={{@closeSearchMenu}}
diff --git a/app/assets/javascripts/discourse/app/components/search-menu/results/random-quick-tip.gjs b/app/assets/javascripts/discourse/app/components/search-menu/results/random-quick-tip.gjs
index 81483049d7957..d38ca488d6487 100644
--- a/app/assets/javascripts/discourse/app/components/search-menu/results/random-quick-tip.gjs
+++ b/app/assets/javascripts/discourse/app/components/search-menu/results/random-quick-tip.gjs
@@ -2,7 +2,6 @@ import Component from "@glimmer/component";
 import { on } from "@ember/modifier";
 import { action } from "@ember/object";
 import { service } from "@ember/service";
-import { focusSearchInput } from "discourse/components/search-menu";
 import concatClass from "discourse/helpers/concat-class";
 import { i18n } from "discourse-i18n";
 
@@ -67,7 +66,7 @@ export default class RandomQuickTip extends Component {
   tipSelected(e) {
     if (e.target.classList.contains("tip-clickable")) {
       this.args.searchTermChanged(this.randomTip.label);
-      focusSearchInput();
+      this.search.focusSearchInput();
 
       e.stopPropagation();
       e.preventDefault();
diff --git a/app/assets/javascripts/discourse/app/components/welcome-banner.gjs b/app/assets/javascripts/discourse/app/components/welcome-banner.gjs
index 2e86ab500a0fe..8b10b87718b58 100644
--- a/app/assets/javascripts/discourse/app/components/welcome-banner.gjs
+++ b/app/assets/javascripts/discourse/app/components/welcome-banner.gjs
@@ -5,7 +5,7 @@ import { htmlSafe } from "@ember/template";
 import { modifier } from "ember-modifier";
 import DButton from "discourse/components/d-button";
 import PluginOutlet from "discourse/components/plugin-outlet";
-import SearchMenu, { focusSearchInput } from "discourse/components/search-menu";
+import SearchMenu from "discourse/components/search-menu";
 import bodyClass from "discourse/helpers/body-class";
 import { prioritizeNameFallback } from "discourse/lib/settings";
 import { applyValueTransformer } from "discourse/lib/transformer";
@@ -39,7 +39,7 @@ export default class WelcomeBanner extends Component {
         (appEvent.type === "search" || appEvent.type === "page-search") &&
         this.search.welcomeBannerSearchInViewport
       ) {
-        focusSearchInput("welcome-banner-search-term");
+        this.search.focusSearchInput();
         appEvent.event.preventDefault();
       }
     };
@@ -107,7 +107,7 @@ export default class WelcomeBanner extends Component {
               />
               <SearchMenu
                 @location="welcome-banner"
-                @searchInputId="welcome-banner-search-term"
+                @searchInputId="welcome-banner-search-input"
               />
             </div>
             <PluginOutlet @name="welcome-banner-below-input" />
diff --git a/app/assets/javascripts/discourse/app/services/search.js b/app/assets/javascripts/discourse/app/services/search.js
index 99ebd13b63355..7a0261e60992e 100644
--- a/app/assets/javascripts/discourse/app/services/search.js
+++ b/app/assets/javascripts/discourse/app/services/search.js
@@ -1,12 +1,13 @@
 import { tracked } from "@glimmer/tracking";
 import { action } from "@ember/object";
 import Service, { service } from "@ember/service";
-import { focusSearchInput } from "discourse/components/search-menu";
 import { disableImplicitInjections } from "discourse/lib/implicit-injections";
+import { applyValueTransformer } from "discourse/lib/transformer";
 
 @disableImplicitInjections
 export default class Search extends Service {
   @service appEvents;
+  @service siteSettings;
 
   @tracked activeGlobalSearchTerm = "";
   @tracked searchContext;
@@ -20,6 +21,27 @@ export default class Search extends Service {
   // only relative for the widget search menu
   searchContextEnabled = false; // checkbox to scope search
 
+  get currentSearchInputId() {
+    if (this.welcomeBannerSearchInViewport) {
+      return "welcome-banner-search-input";
+    } else if (this.searchExperience === "search_field") {
+      return "header-search-input";
+    } else {
+      return "icon-search-input";
+    }
+  }
+
+  get searchExperience() {
+    return applyValueTransformer(
+      "site-setting-search-experience",
+      this.siteSettings.search_experience
+    );
+  }
+
+  focusSearchInput() {
+    document.getElementById(this.currentSearchInputId)?.focus();
+  }
+
   get contextType() {
     return this.searchContext?.type || null;
   }
@@ -97,7 +119,7 @@ export default class Search extends Service {
         const firstResult = results[0] || links[0];
         firstResult.focus();
       } else if (index === 0 && e.key === "ArrowUp") {
-        focusSearchInput();
+        this.focusSearchInput();
       } else if (index > -1) {
         // change focus to the next result item if present
         index += e.key === "ArrowDown" ? 1 : -1;
diff --git a/app/assets/javascripts/discourse/tests/acceptance/group-test.js b/app/assets/javascripts/discourse/tests/acceptance/group-test.js
index c6fb43e15b31f..07441d1955a23 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/group-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/group-test.js
@@ -269,7 +269,7 @@ acceptance("Group - Authenticated", function (needs) {
       );
 
     await click("#search-button");
-    await fillIn("#search-term", "something");
+    await fillIn("#icon-search-input", "something");
 
     assert
       .dom(".search-menu .btn.search-context")
diff --git a/app/assets/javascripts/discourse/tests/acceptance/search-test.js b/app/assets/javascripts/discourse/tests/acceptance/search-test.js
index 16f79fc4705b5..191cf916fea05 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/search-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/search-test.js
@@ -56,14 +56,14 @@ acceptance("Search - Anonymous", function (needs) {
     await visit("/");
     await click("#search-button");
 
-    assert.dom("#search-term").exists("shows the search input");
+    assert.dom("#icon-search-input").exists("shows the search input");
     assert.dom(".show-advanced-search").exists("shows full page search button");
   });
 
   test("random quick tips", async function (assert) {
     await visit("/");
     await click("#search-button");
-    await fillIn("#search-term", "dev");
+    await fillIn("#icon-search-input", "dev");
 
     assert
       .dom(".search-menu .results ul li.search-random-quick-tip")
@@ -73,8 +73,8 @@ acceptance("Search - Anonymous", function (needs) {
   test("advanced search", async function (assert) {
     await visit("/");
     await click("#search-button");
-    await fillIn("#search-term", "dev");
-    await triggerKeyEvent("#search-term", "keyup", "ArrowDown");
+    await fillIn("#icon-search-input", "dev");
+    await triggerKeyEvent("#icon-search-input", "keyup", "ArrowDown");
     await click(document.activeElement);
     await click(".show-advanced-search");
 
@@ -113,7 +113,7 @@ acceptance("Search - Anonymous", function (needs) {
   test("initial options", async function (assert) {
     await visit("/");
     await click("#search-button");
-    await fillIn("#search-term", "dev");
+    await fillIn("#icon-search-input", "dev");
 
     assert
       .dom(
@@ -152,7 +152,7 @@ acceptance("Search - Anonymous", function (needs) {
       .dom(".search-link .search-item-tag")
       .hasText("important", "first option includes tag");
 
-    await fillIn("#search-term", "smth");
+    await fillIn("#icon-search-input", "smth");
     const secondOption = queryAll(contextSelector)[1];
 
     assert
@@ -172,7 +172,7 @@ acceptance("Search - Anonymous", function (needs) {
     const contextSelector = ".search-menu .results .search-menu-assistant-item";
     await visit("/c/bug");
     await click("#search-button");
-    await fillIn("#search-term", "smth");
+    await fillIn("#icon-search-input", "smth");
     const secondOption = queryAll(contextSelector)[1];
 
     assert
@@ -196,7 +196,7 @@ acceptance("Search - Anonymous", function (needs) {
     const contextSelector = ".search-menu .results .search-menu-assistant-item";
     await visit("/t/internationalization-localization/280");
     await click("#search-button");
-    await fillIn("#search-term", "smth");
+    await fillIn("#icon-search-input", "smth");
     const secondOption = queryAll(contextSelector)[1];
 
     assert
@@ -211,7 +211,7 @@ acceptance("Search - Anonymous", function (needs) {
   test("initial options - topic search scope - 'in all topics' searches in all topics", async function (assert) {
     await visit("/t/internationalization-localization/280/1");
     await click("#search-button");
-    await fillIn("#search-term", "foo");
+    await fillIn("#icon-search-input", "foo");
     // select 'in all topics and posts'
     await click(
       ".search-menu .results .search-menu-initial-options .search-menu-assistant-item:first-child"
@@ -224,7 +224,7 @@ acceptance("Search - Anonymous", function (needs) {
   test("initial options - topic search scope - 'in this topic' searches posts within topic", async function (assert) {
     await visit("/t/internationalization-localization/280/1");
     await click("#search-button");
-    await fillIn("#search-term", "foo");
+    await fillIn("#icon-search-input", "foo");
     // select 'in this topic'
     await click(
       ".search-menu .results .search-menu-initial-options .search-menu-assistant-item:nth-child(2)"
@@ -237,7 +237,7 @@ acceptance("Search - Anonymous", function (needs) {
   test("initial options - topic search scope - keep 'in this topic' filter in full page search", async function (assert) {
     await visit("/t/internationalization-localization/280/1");
     await click("#search-button");
-    await fillIn("#search-term", "proper");
+    await fillIn("#icon-search-input", "proper");
     await triggerKeyEvent(document.activeElement, "keyup", "ArrowDown");
     await triggerKeyEvent(document.activeElement, "keydown", "ArrowDown");
     await click(document.activeElement);
@@ -258,7 +258,7 @@ acceptance("Search - Anonymous", function (needs) {
   test("initial options - topic search scope - special case when matching a single user", async function (assert) {
     await visit("/t/internationalization-localization/280/1");
     await click("#search-button");
-    await fillIn("#search-term", "@admin");
+    await fillIn("#icon-search-input", "@admin");
 
     assert.dom(".search-menu-assistant-item").exists({ count: 2 });
     assert
@@ -284,7 +284,7 @@ acceptance("Search - Anonymous", function (needs) {
     const contextSelector = ".search-menu .results .search-menu-assistant-item";
     await visit("/u/eviltrout");
     await click("#search-button");
-    await fillIn("#search-term", "smth");
+    await fillIn("#icon-search-input", "smth");
     const secondOption = queryAll(contextSelector)[1];
 
     assert
@@ -302,8 +302,8 @@ acceptance("Search - Anonymous", function (needs) {
   test("topic results", async function (assert) {
     await visit("/");
     await click("#search-button");
-    await fillIn("#search-term", "dev");
-    await triggerKeyEvent("#search-term", "keyup", "ArrowDown");
+    await fillIn("#icon-search-input", "dev");
+    await triggerKeyEvent("#icon-search-input", "keyup", "ArrowDown");
     await click(document.activeElement);
 
     assert
@@ -318,7 +318,7 @@ acceptance("Search - Anonymous", function (needs) {
   test("topic results - topic search scope", async function (assert) {
     await visit("/t/internationalization-localization/280/1");
     await click("#search-button");
-    await fillIn("#search-term", "a proper");
+    await fillIn("#icon-search-input", "a proper");
     await triggerKeyEvent(document.activeElement, "keyup", "ArrowDown");
     await triggerKeyEvent(document.activeElement, "keydown", "ArrowDown");
     await click(document.activeElement);
@@ -336,15 +336,15 @@ acceptance("Search - Anonymous", function (needs) {
       .exists("search context indicator is visible");
 
     await click(".clear-search");
-    assert.dom("#search-term").hasNoText("clear button works");
+    assert.dom("#icon-search-input").hasNoText("clear button works");
 
     await click(".search-context");
     assert
       .dom(".search-menu .search-context")
       .doesNotExist("search context indicator is no longer visible");
 
-    await fillIn("#search-term", "dev");
-    await focus("#search-term");
+    await fillIn("#icon-search-input", "dev");
+    await focus("#icon-search-input");
     await triggerKeyEvent(document.activeElement, "keyup", "ArrowDown");
     await triggerKeyEvent(document.activeElement, "keydown", "ArrowDown");
     await click(document.activeElement);
@@ -353,9 +353,9 @@ acceptance("Search - Anonymous", function (needs) {
       .dom(".search-menu .search-context")
       .exists("search context indicator is visible");
 
-    await fillIn("#search-term", "");
-    await focus("#search-term");
-    await triggerKeyEvent("#search-term", "keyup", "Backspace");
+    await fillIn("#icon-search-input", "");
+    await focus("#icon-search-input");
+    await triggerKeyEvent("#icon-search-input", "keyup", "Backspace");
 
     assert
       .dom(".search-menu .search-context")
@@ -365,8 +365,8 @@ acceptance("Search - Anonymous", function (needs) {
   test("topic results - search result escapes html in topic title", async function (assert) {
     await visit("/");
     await click("#search-button");
-    await fillIn("#search-term", "dev");
-    await triggerKeyEvent("#search-term", "keyup", "Enter");
+    await fillIn("#icon-search-input", "dev");
+    await triggerKeyEvent("#icon-search-input", "keyup", "Enter");
 
     assert
       .dom(
@@ -378,8 +378,8 @@ acceptance("Search - Anonymous", function (needs) {
   test("topic results - search result escapes emojis in topic title", async function (assert) {
     await visit("/");
     await click("#search-button");
-    await fillIn("#search-term", "dev");
-    await triggerKeyEvent("#search-term", "keyup", "Enter");
+    await fillIn("#icon-search-input", "dev");
+    await triggerKeyEvent("#icon-search-input", "keyup", "Enter");
 
     assert
       .dom(".search-menu .search-result-topic .item .topic-title img[alt='+1']")
@@ -390,7 +390,7 @@ acceptance("Search - Anonymous", function (needs) {
     await visit("/");
     await click("#search-button");
     await focus(".show-advanced-search");
-    await triggerKeyEvent("#search-term", "keydown", "Escape");
+    await triggerKeyEvent("#icon-search-input", "keydown", "Escape");
 
     assert.dom(".search-menu-panel").doesNotExist();
   });
@@ -487,14 +487,14 @@ acceptance("Search - Authenticated", function (needs) {
   test("topic results - topic search scope - works with empty result sets", async function (assert) {
     await visit("/t/internationalization-localization/280");
     await click("#search-button");
-    await fillIn("#search-term", "plans");
-    await triggerKeyEvent("#search-term", "keyup", "ArrowDown");
+    await fillIn("#icon-search-input", "plans");
+    await triggerKeyEvent("#icon-search-input", "keyup", "ArrowDown");
     await click(document.activeElement);
 
     assert.dom(".search-menu .results .item").exists();
 
-    await fillIn("#search-term", "plans empty");
-    await triggerKeyEvent("#search-term", "keyup", 13);
+    await fillIn("#icon-search-input", "plans empty");
+    await triggerKeyEvent("#icon-search-input", "keyup", 13);
 
     assert.dom(".search-menu .results .item").doesNotExist();
     assert.dom(".search-menu .results .no-results").exists({ count: 1 });
@@ -506,7 +506,7 @@ acceptance("Search - Authenticated", function (needs) {
   test("topic results - topic search scope - clicking a search result navigates to topic url", async function (assert) {
     await visit("/");
     await click("#search-button");
-    await fillIn("#search-term", "Development");
+    await fillIn("#icon-search-input", "Development");
     await triggerKeyEvent(document.activeElement, "keyup", "Enter");
 
     assert
@@ -525,8 +525,8 @@ acceptance("Search - Authenticated", function (needs) {
     this.siteSettings.use_pg_headlines_for_excerpt = true;
     await visit("/");
     await click("#search-button");
-    await fillIn("#search-term", "dev");
-    await triggerKeyEvent("#search-term", "keyup", "Enter");
+    await fillIn("#icon-search-input", "dev");
+    await triggerKeyEvent("#icon-search-input", "keyup", "Enter");
 
     assert
       .dom(
@@ -539,13 +539,13 @@ acceptance("Search - Authenticated", function (needs) {
     const container = ".search-menu .results";
     await visit("/");
     await click("#search-button");
-    await fillIn("#search-term", "dev");
+    await fillIn("#icon-search-input", "dev");
     assert.dom(`${container} ul li`).exists("has a list of items");
 
-    await triggerKeyEvent("#search-term", "keyup", "Enter");
+    await triggerKeyEvent("#icon-search-input", "keyup", "Enter");
     assert.dom(`${container} .search-result-topic`).exists("has topic results");
 
-    await triggerKeyEvent("#search-term", "keyup", "ArrowDown");
+    await triggerKeyEvent("#icon-search-input", "keyup", "ArrowDown");
     assert
       .dom(`${container} li:first-child a`)
       .isFocused("arrow down selects first element");
@@ -568,7 +568,7 @@ acceptance("Search - Authenticated", function (needs) {
         "arrow down sets focus to more results link"
       );
 
-    await triggerKeyEvent("#search-term", "keydown", "Escape");
+    await triggerKeyEvent("#icon-search-input", "keydown", "Escape");
     assert
       .dom("#search-button")
       .isFocused("Escaping search returns focus to search button");
@@ -585,10 +585,10 @@ acceptance("Search - Authenticated", function (needs) {
       "arrow up sets focus to search term input"
     );
 
-    await triggerKeyEvent("#search-term", "keyup", "Enter");
+    await triggerKeyEvent("#icon-search-input", "keyup", "Enter");
     assert.dom(`${container} .search-result-topic`).exists("has topic results");
 
-    await triggerKeyEvent("#search-term", "keyup", "Enter");
+    await triggerKeyEvent("#icon-search-input", "keyup", "Enter");
     assert
       .dom(".search-container")
       .exists("second Enter hit goes to full page search");
@@ -600,7 +600,7 @@ acceptance("Search - Authenticated", function (needs) {
     await click("#search-button");
     assert.dom(`${container} ul li`).exists("has a list of items");
 
-    await triggerKeyEvent("#search-term", "keyup", "Enter");
+    await triggerKeyEvent("#icon-search-input", "keyup", "Enter");
     assert.dom(`.search-menu`).exists("search dropdown is visible");
   });
 
@@ -609,8 +609,8 @@ acceptance("Search - Authenticated", function (needs) {
     await click(".reply");
     await fillIn(".d-editor-input", "a link");
     await click("#search-button");
-    await fillIn("#search-term", "dev");
-    await triggerKeyEvent("#search-term", "keyup", "Enter");
+    await fillIn("#icon-search-input", "dev");
+    await triggerKeyEvent("#icon-search-input", "keyup", "Enter");
     await triggerKeyEvent(document.activeElement, "keyup", "ArrowDown");
     await triggerKeyEvent(document.activeElement, "keydown", 65); // maps to lowercase a
 
@@ -631,7 +631,7 @@ acceptance("Search - Authenticated", function (needs) {
     await visit("/");
     await triggerKeyEvent(document, "keypress", "J");
     await click("#search-button");
-    await fillIn("#search-term", "Development");
+    await fillIn("#icon-search-input", "Development");
     await triggerKeyEvent(document.activeElement, "keyup", "Enter");
     await triggerKeyEvent(document.activeElement, "keyup", "ArrowDown");
 
@@ -667,7 +667,7 @@ acceptance("Search - Authenticated", function (needs) {
       ".search-menu .search-menu-recent li:nth-of-type(1) .search-link"
     );
 
-    assert.dom("input#search-term").hasValue("yellow");
+    assert.dom("input#icon-search-input").hasValue("yellow");
   });
 
   test("initial options - overriding behavior with addSearchMenuAssistantSelectCallback", async function (assert) {
@@ -689,7 +689,7 @@ acceptance("Search - Authenticated", function (needs) {
       ".search-menu .search-menu-recent li:nth-of-type(1) .search-link"
     );
 
-    assert.dom("#search-term").hasValue("hijacked!");
+    assert.dom("#icon-search-input").hasValue("hijacked!");
   });
 
   test("initial options - search history - category context", async function (assert) {
@@ -774,8 +774,8 @@ acceptance("Search - with tagging enabled", function (needs) {
   test("topic results - displays tags", async function (assert) {
     await visit("/");
     await click("#search-button");
-    await fillIn("#search-term", "dev");
-    await triggerKeyEvent("#search-term", "keyup", 13);
+    await fillIn("#icon-search-input", "dev");
+    await triggerKeyEvent("#icon-search-input", "keyup", 13);
 
     assert
       .dom(".search-menu .results ul li:nth-of-type(1) .discourse-tags")
@@ -785,7 +785,7 @@ acceptance("Search - with tagging enabled", function (needs) {
   test("initial options - topic search scope - selecting a tag defaults to searching 'in all topics'", async function (assert) {
     await visit("/t/internationalization-localization/280/1");
     await click("#search-button");
-    await fillIn("#search-term", "#dev");
+    await fillIn("#icon-search-input", "#dev");
     await click(
       ".search-menu .results .search-menu-assistant .search-menu-assistant-item:nth-child(1)"
     );
@@ -797,7 +797,7 @@ acceptance("Search - with tagging enabled", function (needs) {
   test("initial results - displays tag shortcuts", async function (assert) {
     await visit("/");
     await click("#search-button");
-    await fillIn("#search-term", "dude #monk");
+    await fillIn("#icon-search-input", "dude #monk");
     const firstItem =
       ".search-menu .results ul.search-menu-assistant .search-link";
 
@@ -832,7 +832,7 @@ acceptance("Search - with tagging enabled", function (needs) {
   test("initial options - tag and category search scope - updates tag / category combination search suggestion when typing", async function (assert) {
     await visit("/tags/c/bug/dev");
     await click("#search-button");
-    await fillIn("#search-term", "foo bar");
+    await fillIn("#icon-search-input", "foo bar");
 
     assert
       .dom(".search-menu .results ul.search-menu-assistant .search-item-prefix")
@@ -870,7 +870,7 @@ acceptance("Search - with tagging enabled", function (needs) {
   test("initial options - tag intersection search scope - updates tag intersection search suggestion when typing", async function (assert) {
     await visit("/tags/intersection/dev/foo");
     await click("#search-button");
-    await fillIn("#search-term", "foo bar");
+    await fillIn("#icon-search-input", "foo bar");
 
     assert
       .dom(".search-menu .results ul.search-menu-assistant .search-item-prefix")
@@ -1062,7 +1062,7 @@ acceptance("Search - assistant", function (needs) {
   test("initial options - shows category shortcuts when typing #", async function (assert) {
     await visit("/");
     await click("#search-button");
-    await fillIn("#search-term", "#");
+    await fillIn("#icon-search-input", "#");
 
     assert
       .dom(
@@ -1077,12 +1077,12 @@ acceptance("Search - assistant", function (needs) {
     const firstTarget =
       ".search-menu .results ul.search-menu-assistant .search-link";
 
-    await fillIn("#search-term", "in:");
+    await fillIn("#icon-search-input", "in:");
     assert
       .dom(`${firstTarget} .search-item-slug`)
       .hasText("in:title", "keyword is present in suggestion");
 
-    await fillIn("#search-term", "sam in:");
+    await fillIn("#icon-search-input", "sam in:");
     assert
       .dom(`${firstTarget} .search-item-prefix`)
       .hasText("sam", "term is present in suggestion");
@@ -1090,14 +1090,14 @@ acceptance("Search - assistant", function (needs) {
       .dom(`${firstTarget} .search-item-slug`)
       .hasText("in:title", "keyword is present in suggestion");
 
-    await fillIn("#search-term", "in:mess");
+    await fillIn("#icon-search-input", "in:mess");
     assert.dom(firstTarget).hasText("in:messages");
   });
 
   test("initial options - user search scope - shows users when typing @", async function (assert) {
     await visit("/");
     await click("#search-button");
-    await fillIn("#search-term", "@");
+    await fillIn("#icon-search-input", "@");
     assert
       .dom(
         ".search-menu .results ul.search-menu-assistant .search-item-user .username"
@@ -1114,13 +1114,13 @@ acceptance("Search - assistant", function (needs) {
       ".search-menu .results ul.search-menu-assistant .search-item-user"
     );
 
-    assert.dom("#search-term").hasValue(`@${username}`);
+    assert.dom("#icon-search-input").hasValue(`@${username}`);
   });
 
   test("initial options - topic search scope - selecting a tag defaults to searching 'in all topics'", async function (assert) {
     await visit("/t/internationalization-localization/280/1");
     await click("#search-button");
-    await fillIn("#search-term", "@");
+    await fillIn("#icon-search-input", "@");
     await click(
       ".search-menu .results .search-menu-assistant .search-menu-assistant-item:nth-child(1)"
     );
@@ -1154,9 +1154,9 @@ acceptance("Search - assistant", function (needs) {
 
     assert.dom(".btn.search-context").exists("shows the button");
 
-    await fillIn("#search-term", "");
-    await focus("input#search-term");
-    await triggerKeyEvent("input#search-term", "keyup", "Backspace");
+    await fillIn("#icon-search-input", "");
+    await focus("input#icon-search-input");
+    await triggerKeyEvent("input#icon-search-input", "keyup", "Backspace");
 
     assert.dom(".btn.search-context").doesNotExist("removes the button");
 
@@ -1166,9 +1166,9 @@ acceptance("Search - assistant", function (needs) {
       .dom(".btn.search-context")
       .exists("shows the button when reinvoking search");
 
-    await fillIn("#search-term", "emoji");
-    await focus("input#search-term");
-    await triggerKeyEvent("#search-term", "keyup", "Enter");
+    await fillIn("#icon-search-input", "emoji");
+    await focus("input#icon-search-input");
+    await triggerKeyEvent("#icon-search-input", "keyup", "Enter");
 
     assert
       .dom(".search-menu .search-result-topic")
@@ -1178,7 +1178,7 @@ acceptance("Search - assistant", function (needs) {
   test("topic results - updates search term when selecting an initial category option", async function (assert) {
     await visit("/");
     await click("#search-button");
-    await fillIn("#search-term", "sam #");
+    await fillIn("#icon-search-input", "sam #");
 
     const firstCategory =
       ".search-menu .results ul.search-menu-assistant .search-link";
@@ -1188,13 +1188,13 @@ acceptance("Search - assistant", function (needs) {
 
     await click(`${firstCategory} .badge-category__name`);
 
-    assert.dom("#search-term").hasValue(`sam #${firstCategoryName}`);
+    assert.dom("#icon-search-input").hasValue(`sam #${firstCategoryName}`);
   });
 
   test("topic results - soft loads the topic results after closing the search menu", async function (assert) {
     await visit("/");
     await click("#search-button");
-    await fillIn("#search-term", "Development mode");
+    await fillIn("#icon-search-input", "Development mode");
 
     // navigate to topic and close search menu
     const firstTopicResult = ".search-menu .results .search-result-topic a";
diff --git a/app/assets/javascripts/discourse/tests/integration/components/search-menu-test.gjs b/app/assets/javascripts/discourse/tests/integration/components/search-menu-test.gjs
index 77379fb0bc97c..ea5233dfe17cd 100644
--- a/app/assets/javascripts/discourse/tests/integration/components/search-menu-test.gjs
+++ b/app/assets/javascripts/discourse/tests/integration/components/search-menu-test.gjs
@@ -29,7 +29,11 @@ module("Integration | Component | search-menu", function (hooks) {
       return response(searchFixtures["search/query"]);
     });
 
-    await render(<template><SearchMenu @location="test" /></template>);
+    await render(
+      <template>
+        <SearchMenu @location="test" @searchInputId="icon-search-input" />
+      </template>
+    );
 
     assert
       .dom(".show-advanced-search")
@@ -37,13 +41,13 @@ module("Integration | Component | search-menu", function (hooks) {
 
     assert.dom(".menu-panel").doesNotExist("Menu panel is not rendered yet");
 
-    await click("#search-term");
+    await click("#icon-search-input");
 
     assert
       .dom(".menu-panel .search-menu-initial-options")
       .exists("Menu panel is rendered with initial options");
 
-    await fillIn("#search-term", "test");
+    await fillIn("#icon-search-input", "test");
 
     assert
       .dom(".label-suffix")
@@ -52,18 +56,18 @@ module("Integration | Component | search-menu", function (hooks) {
         "search label reflects context of search"
       );
 
-    await triggerKeyEvent("#search-term", "keyup", "Enter");
+    await triggerKeyEvent("#icon-search-input", "keyup", "Enter");
 
     assert
       .dom(".search-result-topic")
       .exists("search result is a list of topics");
 
-    await triggerKeyEvent("#search-term", "keydown", "Escape");
+    await triggerKeyEvent("#icon-search-input", "keydown", "Escape");
 
     assert.dom(".menu-panel").doesNotExist("Menu panel is gone");
 
-    await click("#search-term");
-    await click("#search-term");
+    await click("#icon-search-input");
+    await click("#icon-search-input");
 
     assert
       .dom(".search-result-topic")
@@ -73,13 +77,16 @@ module("Integration | Component | search-menu", function (hooks) {
   test("clicking outside results hides and blurs input", async function (assert) {
     await render(
       <template>
-        <div id="click-me"><SearchMenu @location="test" /></div>
+        <div id="click-me"><SearchMenu
+            @location="test"
+            @searchInputId="icon-search-input"
+          /></div>
       </template>
     );
-    await click("#search-term");
+    await click("#icon-search-input");
 
     assert
-      .dom("#search-term")
+      .dom("#icon-search-input")
       .isFocused("Clicking the search term input focuses it");
 
     await click("#click-me");
diff --git a/app/assets/stylesheets/common/base/search-menu.scss b/app/assets/stylesheets/common/base/search-menu.scss
index 53152d5ff6da2..ffa7e8439377b 100644
--- a/app/assets/stylesheets/common/base/search-menu.scss
+++ b/app/assets/stylesheets/common/base/search-menu.scss
@@ -30,7 +30,6 @@ $search-pad-horizontal: 0.5em;
     margin: 1px;
     padding: 0.25rem;
 
-    input#search-term,
     input.search-term__input {
       background: none;
       border: 0;
diff --git a/app/assets/stylesheets/common/components/welcome-banner.scss b/app/assets/stylesheets/common/components/welcome-banner.scss
index 7d8416eadd11b..40c8fa4ae120f 100644
--- a/app/assets/stylesheets/common/components/welcome-banner.scss
+++ b/app/assets/stylesheets/common/components/welcome-banner.scss
@@ -59,7 +59,6 @@
     }
 
     .search-input {
-      #search-term,
       .search-term__input {
         min-width: 0;
         flex: 1 1;
diff --git a/app/assets/stylesheets/desktop/components/header-search.scss b/app/assets/stylesheets/desktop/components/header-search.scss
index 10db1fe364569..1d5a5fe8d0c8f 100644
--- a/app/assets/stylesheets/desktop/components/header-search.scss
+++ b/app/assets/stylesheets/desktop/components/header-search.scss
@@ -59,7 +59,6 @@
             display: none;
           }
 
-          .search-input .search-context ~ #search-term,
           .search-input .search-context ~ .search-term__input {
             padding-left: 0.5em;
           }
@@ -99,7 +98,6 @@
             padding: 0 0 0 1.5em;
             border-radius: var(--d-input-border-radius);
 
-            #search-term,
             .search-term__input {
               height: 100%;
               width: 100%;
diff --git a/app/assets/stylesheets/mobile/components/welcome-banner.scss b/app/assets/stylesheets/mobile/components/welcome-banner.scss
index 96289dd60ee95..8fc028957b7bb 100644
--- a/app/assets/stylesheets/mobile/components/welcome-banner.scss
+++ b/app/assets/stylesheets/mobile/components/welcome-banner.scss
@@ -3,10 +3,6 @@
     display: inline-block;
   }
 
-  .search-menu .search-input input#search-term {
-    width: 100%;
-  }
-
   .search-menu .search-input input.search-term__input {
     width: 100%;
   }
diff --git a/spec/system/page_objects/pages/search.rb b/spec/system/page_objects/pages/search.rb
index 1b392a889a580..f6c71094d9868 100644
--- a/spec/system/page_objects/pages/search.rb
+++ b/spec/system/page_objects/pages/search.rb
@@ -103,8 +103,8 @@ def has_found_no_results?
         page.has_css?(".search-menu-container .results .no-results")
       end
 
-      def search_term
-        page.find("#search-term").value
+      def search_term(id = "icon-search-input")
+        page.find("##{id}").value
       end
 
       SEARCH_PAGE_SELECTOR = "body.search-page"
diff --git a/spec/system/post_small_action_spec.rb b/spec/system/post_small_action_spec.rb
index 47a5f324cf6be..12ec72fc503ec 100644
--- a/spec/system/post_small_action_spec.rb
+++ b/spec/system/post_small_action_spec.rb
@@ -21,7 +21,7 @@
     expect(topic_page).to have_post_number(post.post_number)
 
     find(".search-dropdown").click
-    find("#search-term").fill_in(with: "special")
+    find("#icon-search-input").fill_in(with: "special")
 
     find(".search-menu-assistant-item:nth-child(2)").click
 
diff --git a/spec/system/search_shortcut_variation_spec.rb b/spec/system/search_shortcut_variation_spec.rb
index 7c706063a438c..f5995a99d415a 100644
--- a/spec/system/search_shortcut_variation_spec.rb
+++ b/spec/system/search_shortcut_variation_spec.rb
@@ -19,7 +19,7 @@
         expect(welcome_banner).to be_visible
         page.send_keys("/")
         expect(search_page).to have_search_menu
-        expect(current_active_element[:id]).to eq("welcome-banner-search-term")
+        expect(current_active_element[:id]).to eq("welcome-banner-search-input")
         page.send_keys(:escape)
         expect(search_page).to have_no_search_menu_visible
       end
@@ -29,11 +29,24 @@
         expect(welcome_banner).to be_visible
         search_page.browser_search_shortcut
         expect(search_page).to have_search_menu
-        expect(current_active_element[:id]).to eq("welcome-banner-search-term")
+        expect(current_active_element[:id]).to eq("welcome-banner-search-input")
         page.send_keys(:escape)
         expect(search_page).to have_no_search_menu_visible
       end
 
+      it "displays and focuses welcome banner search when Ctrl+F is pressed and blurs it when Ctrl+F is pressed" do
+        visit("/")
+        expect(welcome_banner).to be_visible
+        search_page.browser_search_shortcut
+        expect(search_page).to have_search_menu
+        expect(current_active_element[:id]).to eq("welcome-banner-search-input")
+        # NOTE: This does not work as expected because pressing Ctrl+F in the browser does
+        # not change the document acive element, leaving it here as a reminder to manually test
+        # this behavior.
+        # search_page.browser_search_shortcut
+        # expect(current_active_element[:id]).to eq(nil)
+      end
+
       context "when welcome banner is not in the viewport" do
         before do
           visit("/")
@@ -44,7 +57,7 @@
           expect(welcome_banner).to be_invisible
           page.send_keys("/")
           expect(search_page).to have_search_menu
-          expect(current_active_element[:id]).to eq("search-term")
+          expect(current_active_element[:id]).to eq("header-search-input")
           page.send_keys(:escape)
           expect(search_page).to have_no_search_menu_visible
         end
@@ -53,10 +66,22 @@
           expect(welcome_banner).to be_invisible
           search_page.browser_search_shortcut
           expect(search_page).to have_search_menu
-          expect(current_active_element[:id]).to eq("search-term")
+          expect(current_active_element[:id]).to eq("header-search-input")
           page.send_keys(:escape)
           expect(search_page).to have_no_search_menu_visible
         end
+
+        it "displays and focuses welcome banner search when Ctrl+F is pressed and blurs it when Ctrl+F is pressed" do
+          expect(welcome_banner).to be_invisible
+          search_page.browser_search_shortcut
+          expect(search_page).to have_search_menu
+          expect(current_active_element[:id]).to eq("header-search-input")
+          # NOTE: This does not work as expected because pressing Ctrl+F in the browser does
+          # not change the document acive element, leaving it here as a reminder to manually test
+          # this behavior.
+          # search_page.browser_search_shortcut
+          # expect(current_active_element[:id]).to eq(nil)
+        end
       end
     end
 
@@ -68,7 +93,7 @@
         expect(welcome_banner).to be_hidden
         page.send_keys("/")
         expect(search_page).to have_search_menu
-        expect(current_active_element[:id]).to eq("search-term")
+        expect(current_active_element[:id]).to eq("header-search-input")
         page.send_keys(:escape)
         expect(search_page).to have_no_search_menu_visible
       end
@@ -78,7 +103,7 @@
         expect(welcome_banner).to be_hidden
         search_page.browser_search_shortcut
         expect(search_page).to have_search_menu
-        expect(current_active_element[:id]).to eq("search-term")
+        expect(current_active_element[:id]).to eq("header-search-input")
         page.send_keys(:escape)
         expect(search_page).to have_no_search_menu_visible
       end
@@ -96,7 +121,7 @@
         expect(welcome_banner).to be_visible
         page.send_keys("/")
         expect(search_page).to have_search_menu
-        expect(current_active_element[:id]).to eq("welcome-banner-search-term")
+        expect(current_active_element[:id]).to eq("welcome-banner-search-input")
         page.send_keys(:escape)
         expect(search_page).to have_no_search_menu_visible
       end
@@ -106,11 +131,25 @@
         expect(welcome_banner).to be_visible
         search_page.browser_search_shortcut
         expect(search_page).to have_search_menu
-        expect(current_active_element[:id]).to eq("welcome-banner-search-term")
+        expect(current_active_element[:id]).to eq("welcome-banner-search-input")
         page.send_keys(:escape)
         expect(search_page).to have_no_search_menu_visible
       end
 
+      it "displays and focuses welcome banner search when Ctrl+F is pressed and blurs it when Ctrl+F is pressed" do
+        visit("/")
+        expect(welcome_banner).to be_visible
+        search_page.browser_search_shortcut
+        expect(search_page).to have_search_menu
+        expect(current_active_element[:id]).to eq("welcome-banner-search-input")
+
+        # NOTE: This does not work as expected because pressing Ctrl+F in the browser does
+        # not change the document acive element, leaving it here as a reminder to manually test
+        # this behavior.
+        # search_page.browser_search_shortcut
+        # expect(current_active_element[:id]).to eq(nil)
+      end
+
       context "when welcome banner is not in the viewport" do
         before do
           visit("/")
@@ -121,7 +160,7 @@
           expect(welcome_banner).to be_invisible
           page.send_keys("/")
           expect(search_page).to have_search_menu
-          expect(current_active_element[:id]).to eq("search-term")
+          expect(current_active_element[:id]).to eq("icon-search-input")
           page.send_keys(:escape)
           expect(search_page).to have_no_search_menu_visible
         end
@@ -136,7 +175,7 @@
         expect(welcome_banner).to be_hidden
         page.send_keys("/")
         expect(search_page).to have_search_menu
-        expect(current_active_element[:id]).to eq("search-term")
+        expect(current_active_element[:id]).to eq("icon-search-input")
         page.send_keys(:escape)
         expect(search_page).to have_no_search_menu_visible
       end
@@ -151,8 +190,12 @@
           visit "/t/#{topic.slug}/#{topic.id}"
           search_page.browser_search_shortcut
           expect(search_page).to have_search_menu_visible
-          search_page.browser_search_shortcut
-          expect(search_page).to have_no_search_menu_visible
+          expect(current_active_element[:id]).to eq("icon-search-input")
+          # NOTE: This does not work as expected because pressing Ctrl+F in the browser does
+          # not change the document acive element, leaving it here as a reminder to manually test
+          # this behavior.
+          # search_page.browser_search_shortcut
+          # expect(current_active_element[:id]).to eq(nil)
         end
       end
     end


Reviews:
user: login: Drenmi
body: LGTM! 🔍 
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T06:05:01Z
user: login: martin-brennan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T00:50:57Z
user: login: Drenmi
body: 🚀 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T01:21:12Z

Review comments:
diff_hunk: @@ -20,6 +21,27 @@ export default class Search extends Service {
   // only relative for the widget search menu
   searchContextEnabled = false; // checkbox to scope search
 
+  get currentSearchInputId() {
+    if (this.welcomeBannerSearchInViewport) {
+      return "welcome-banner-search-input";
+    } else if (this.searchExperience === "search_field") {
+      return "header-search-input";
+    } else {
+      return "icon-search-input";
+    }
+  }
+
+  get searchExperience() {
+    return applyValueTransformer(
+      "site-setting-search-experience",
+      this.siteSettings.search_experience
+    );
+  }
+
+  focusSearchInput() {
+    document.getElementById(this.currentSearchInputId).focus();
path: app/assets/javascripts/discourse/app/services/search.js
user: login: Drenmi
body: How stable is the assumption that one of the element IDs can be found? Would it be worth doing a safe navigation call to `#focus`?

```suggestion
    document.getElementById(this.currentSearchInputId)?.focus();
```
created_at: 2025-04-03T06:04:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,6 +21,27 @@ export default class Search extends Service {
   // only relative for the widget search menu
   searchContextEnabled = false; // checkbox to scope search
 
+  get currentSearchInputId() {
+    if (this.welcomeBannerSearchInViewport) {
+      return "welcome-banner-search-input";
+    } else if (this.searchExperience === "search_field") {
+      return "header-search-input";
+    } else {
+      return "icon-search-input";
+    }
+  }
+
+  get searchExperience() {
+    return applyValueTransformer(
+      "site-setting-search-experience",
+      this.siteSettings.search_experience
+    );
+  }
+
+  focusSearchInput() {
+    document.getElementById(this.currentSearchInputId).focus();
path: app/assets/javascripts/discourse/app/services/search.js
user: login: martin-brennan
body: Yep fair
created_at: 2025-04-04T00:50:57Z
author_association: CONTRIBUTOR

PR comments:
user: login: martin-brennan
created_at: 2025-04-03T04:55:38Z
body: This commit fixes the theme spec issues https://github.com/discourse/discourse-search-banner/pull/87
author_association: CONTRIBUTOR
user: login: discoursebot
created_at: 2025-04-03T16:48:13Z
body: This pull request has been mentioned on **Discourse Meta**. There might be relevant details there:

https://meta.discourse.org/t/the-welcome-banner-search-input-loses-focus-after-selecting-a-result/359934/5

author_association: NONE
user: login: nolosb
created_at: 2025-04-03T16:55:22Z
body: Just adding here that there seem to be 4 variations:

    Header search
    Welcome banner search (from core)
    Advanced Search banner (component)
    Icon search

And right now the core banner gets focus, while the component one doesn't.
author_association: MEMBER
user: login: martin-brennan
created_at: 2025-04-04T00:46:31Z
body: @nolosb the component issue should be fixed by https://github.com/discourse/discourse-search-banner/pull/87
author_association: CONTRIBUTOR