From 832ed8ce748982311e5ed654a9110fc608a03bf8 Mon Sep 17 00:00:00 2001 From: Martin Brennan Date: Fri, 21 Mar 2025 09:20:58 +1000 Subject: [PATCH] UX: Fix various search shortcut UX issues (#31903) Now we have the search input showing in a few different configurations: * Welcome banner * Header field * Header icon And we can get to the search with both `/` and `Ctrl+F` shortcuts. These configurations can be used together, and we need to focus on the right search input at the right time. This commit fixes the shortcuts not working or showing the wrong thing in some cases, and adds a comprehensive system spec for all the variants. --- .../discourse/app/components/header.gjs | 7 +- .../app/components/header/header-search.gjs | 11 +- .../discourse/app/components/search-menu.gjs | 9 +- .../components/search-menu/search-term.gjs | 3 +- .../app/components/welcome-banner.gjs | 33 +++- .../discourse/app/services/search.js | 1 + .../stylesheets/common/base/search-menu.scss | 3 +- .../common/components/welcome-banner.scss | 3 +- .../desktop/components/header-search.scss | 6 +- .../mobile/components/welcome-banner.scss | 4 + spec/support/system_helpers.rb | 15 ++ spec/system/header_spec.rb | 13 -- spec/system/page_objects/pages/header.rb | 6 +- spec/system/page_objects/pages/search.rb | 15 +- spec/system/search_shortcut_variation_spec.rb | 160 ++++++++++++++++++ spec/system/welcome_banner_spec.rb | 4 +- 16 files changed, 253 insertions(+), 40 deletions(-) create mode 100644 spec/system/search_shortcut_variation_spec.rb diff --git a/app/assets/javascripts/discourse/app/components/header.gjs b/app/assets/javascripts/discourse/app/components/header.gjs index 076cdf36c01..d69a899ce1e 100644 --- a/app/assets/javascripts/discourse/app/components/header.gjs +++ b/app/assets/javascripts/discourse/app/components/header.gjs @@ -129,7 +129,12 @@ export default class GlimmerHeader extends Component { headerKeyboardTrigger(msg) { switch (msg.type) { case "search": - this.toggleSearchMenu(); + // This must be done here because toggleSearchMenu is + // also called from the search button, we only want to + // stop it using the shortcut. + if (!this.search.welcomeBannerSearchInViewport) { + this.toggleSearchMenu(); + } break; case "user": this.toggleUserMenu(); 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 f8e8a2962f7..8f7c35baa7d 100644 --- a/app/assets/javascripts/discourse/app/components/header/header-search.gjs +++ b/app/assets/javascripts/discourse/app/components/header/header-search.gjs @@ -1,6 +1,6 @@ import Component from "@glimmer/component"; import { service } from "@ember/service"; -import { modifier as modifierFn } from "ember-modifier"; +import { modifier } from "ember-modifier"; import DButton from "discourse/components/d-button"; import SearchMenu, { focusSearchInput } from "discourse/components/search-menu"; import bodyClass from "discourse/helpers/body-class"; @@ -14,8 +14,13 @@ export default class HeaderSearch extends Component { advancedSearchButtonHref = "/search?expanded=true"; - handleKeyboardShortcut = modifierFn(() => { - const cb = () => focusSearchInput(); + handleKeyboardShortcut = modifier(() => { + const cb = (appEvent) => { + if (appEvent.type === "search" || appEvent.type === "page-search") { + focusSearchInput(); + appEvent.event.preventDefault(); + } + }; this.appEvents.on("header:keyboard-trigger", cb); return () => this.appEvents.off("header:keyboard-trigger", cb); }); diff --git a/app/assets/javascripts/discourse/app/components/search-menu.gjs b/app/assets/javascripts/discourse/app/components/search-menu.gjs index 5a1f1faf15c..b23991015b1 100644 --- a/app/assets/javascripts/discourse/app/components/search-menu.gjs +++ b/app/assets/javascripts/discourse/app/components/search-menu.gjs @@ -37,8 +37,8 @@ export const SEARCH_INPUT_ID = "search-term"; export const MODIFIER_REGEXP = /.*(\#|\@|:).*$/gi; export const DEFAULT_TYPE_FILTER = "exclude_topics"; -export function focusSearchInput() { - document.getElementById(SEARCH_INPUT_ID).focus(); +export function focusSearchInput(inputId = SEARCH_INPUT_ID) { + document.getElementById(inputId).focus(); } export default class SearchMenu extends Component { @@ -121,6 +121,8 @@ export default class SearchMenu extends Component { onKeydown(event) { if (event.key === "Escape") { this.close(); + event.preventDefault(); + event.stopPropagation(); } } @@ -132,7 +134,7 @@ export default class SearchMenu extends Component { // We want to blur the search input when in stand-alone mode // so that when we focus on the search input again, the menu panel pops up - document.getElementById(SEARCH_INPUT_ID)?.blur(); + document.getElementById(this.args.searchInputId || SEARCH_INPUT_ID)?.blur(); this.menuPanelOpen = false; } @@ -430,6 +432,7 @@ export default class SearchMenu extends Component { @closeSearchMenu={{this.close}} @openSearchMenu={{this.open}} @autofocus={{@autofocusInput}} + @inputId={{@searchInputId}} /> {{#if this.loading}} diff --git a/app/assets/javascripts/discourse/app/components/search-menu/search-term.gjs b/app/assets/javascripts/discourse/app/components/search-menu/search-term.gjs index bd6eb1f362e..53d3b1c9178 100644 --- a/app/assets/javascripts/discourse/app/components/search-menu/search-term.gjs +++ b/app/assets/javascripts/discourse/app/components/search-menu/search-term.gjs @@ -31,7 +31,7 @@ export default class SearchTerm extends Component { // make constant available in template get inputId() { - return SEARCH_INPUT_ID; + return this.args.inputId || SEARCH_INPUT_ID; } @action @@ -121,6 +121,7 @@ export default class SearchTerm extends Component {