diff --git a/app/assets/javascripts/discourse/app/components/search-menu-panel.hbs b/app/assets/javascripts/discourse/app/components/search-menu-panel.hbs new file mode 100644 index 00000000000..51d562c165e --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/search-menu-panel.hbs @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/app/assets/javascripts/discourse/app/components/search-menu-panel.js b/app/assets/javascripts/discourse/app/components/search-menu-panel.js new file mode 100644 index 00000000000..f6d907485a4 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/search-menu-panel.js @@ -0,0 +1,11 @@ +import Component from "@glimmer/component"; +import { inject as service } from "@ember/service"; + +export default class SearchMenuPanel extends Component { + @service site; + get animationClass() { + return this.site.mobileView || this.site.narrowDesktopView + ? "slide-in" + : "drop-down"; + } +} diff --git a/app/assets/javascripts/discourse/app/components/search-menu.hbs b/app/assets/javascripts/discourse/app/components/search-menu.hbs index fbb0cc04ae5..ce806cafe0a 100644 --- a/app/assets/javascripts/discourse/app/components/search-menu.hbs +++ b/app/assets/javascripts/discourse/app/components/search-menu.hbs @@ -1,23 +1,83 @@ - - - \ No newline at end of file +
+
+ {{#if this.search.inTopicContext}} + + {{else if this.inPMInboxContext}} + + {{/if}} + + + + {{#if this.loading}} +
+ {{loading-spinner}} +
+ {{else}} +
+ {{#if this.search.activeGlobalSearchTerm}} + + {{/if}} + +
+ {{/if}} +
+ + {{#if @inlineResults}} + + {{else if this.displayMenuPanelResults}} + + + + {{/if}} +
\ No newline at end of file diff --git a/app/assets/javascripts/discourse/app/components/search-menu.js b/app/assets/javascripts/discourse/app/components/search-menu.js index 49cd6e296c7..e979d51d0d4 100644 --- a/app/assets/javascripts/discourse/app/components/search-menu.js +++ b/app/assets/javascripts/discourse/app/components/search-menu.js @@ -22,7 +22,6 @@ 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_BUTTON_ID = "search-button"; export const MODIFIER_REGEXP = /.*(\#|\@|:).*$/gi; export const DEFAULT_TYPE_FILTER = "exclude_topics"; @@ -30,16 +29,11 @@ export function focusSearchInput() { document.getElementById(SEARCH_INPUT_ID).focus(); } -export function focusSearchButton() { - document.getElementById(SEARCH_BUTTON_ID).focus(); -} - export default class SearchMenu extends Component { @service search; @service currentUser; @service siteSettings; @service appEvents; - @service site; @tracked loading = false; @tracked results = {}; @@ -50,13 +44,42 @@ export default class SearchMenu extends Component { @tracked suggestionKeyword = false; @tracked suggestionResults = []; @tracked invalidTerm = false; + @tracked menuPanelOpen = false; + _debouncer = null; _activeSearch = null; - get animationClass() { - return this.site.mobileView || this.site.narrowDesktopView - ? "slide-in" - : "drop-down"; + @bind + setupEventListeners() { + document.addEventListener("mousedown", this.onDocumentPress, true); + document.addEventListener("touchend", this.onDocumentPress, { + capture: true, + passive: true, + }); + } + + willDestroy() { + document.removeEventListener("mousedown", this.onDocumentPress); + document.removeEventListener("touchend", this.onDocumentPress); + + super.willDestroy(...arguments); + } + + @bind + onDocumentPress(event) { + if (!event.target.closest(".search-menu-container.menu-panel-results")) { + this.menuPanelOpen = false; + } + } + + get classNames() { + const classes = ["search-menu-container"]; + + if (!this.args.inlineResults) { + classes.push("menu-panel-results"); + } + + return classes.join(" "); } get includesTopics() { @@ -71,6 +94,23 @@ export default class SearchMenu extends Component { return false; } + @action + close() { + if (this.args?.closeSearchMenu) { + return this.args.closeSearchMenu(); + } + + // We want to blur the active element (search input) when in stand-alone mode + // so that when we focus on the search input again, the menu panel pops up + document.activeElement.blur(); + this.menuPanelOpen = false; + } + + @action + open() { + this.menuPanelOpen = true; + } + @bind fullSearchUrl(opts) { let url = "/search"; @@ -95,6 +135,18 @@ export default class SearchMenu extends Component { return getURL(url); } + get advancedSearchButtonHref() { + return this.fullSearchUrl({ expanded: true }); + } + + get displayMenuPanelResults() { + if (this.args.inlineResults) { + return false; + } + + return this.menuPanelOpen; + } + @bind clearSearch(e) { e.stopPropagation(); diff --git a/app/assets/javascripts/discourse/app/components/search-menu/menu-panel-contents.hbs b/app/assets/javascripts/discourse/app/components/search-menu/menu-panel-contents.hbs deleted file mode 100644 index 9caee2bb218..00000000000 --- a/app/assets/javascripts/discourse/app/components/search-menu/menu-panel-contents.hbs +++ /dev/null @@ -1,63 +0,0 @@ -
- {{#if this.search.inTopicContext}} - - {{else if @inPMInboxContext}} - - {{/if}} - - - - {{#if @loading}} -
- {{loading-spinner}} -
- {{else}} -
- {{#if this.search.activeGlobalSearchTerm}} - - {{/if}} - -
- {{/if}} -
- -{{#if (and this.search.inTopicContext (not @includesTopics))}} - -{{else}} - {{#unless @loading}} - - {{/unless}} -{{/if}} \ No newline at end of file diff --git a/app/assets/javascripts/discourse/app/components/search-menu/menu-panel-contents.js b/app/assets/javascripts/discourse/app/components/search-menu/menu-panel-contents.js deleted file mode 100644 index e980911f3a9..00000000000 --- a/app/assets/javascripts/discourse/app/components/search-menu/menu-panel-contents.js +++ /dev/null @@ -1,10 +0,0 @@ -import Component from "@glimmer/component"; -import { inject as service } from "@ember/service"; - -export default class MenuPanelContents extends Component { - @service search; - - get advancedSearchButtonHref() { - return this.args.fullSearchUrl({ expanded: true }); - } -} diff --git a/app/assets/javascripts/discourse/app/components/search-menu/results.hbs b/app/assets/javascripts/discourse/app/components/search-menu/results.hbs index 67032e7281e..46a4e2442c3 100644 --- a/app/assets/javascripts/discourse/app/components/search-menu/results.hbs +++ b/app/assets/javascripts/discourse/app/components/search-menu/results.hbs @@ -1,52 +1,56 @@ -
- {{#if @suggestionKeyword}} - - {{else if this.termTooShort}} -
{{i18n "search.too_short"}}
- {{else if this.noTopicResults}} -
{{i18n "search.no_results"}}
- {{else if this.renderInitialOptions}} - - {{else}} - {{#if @searchTopics}} - {{! render results after a search has been performed }} - {{#if this.resultTypesWithComponent}} - - - {{/if}} +{{#if (and this.search.inTopicContext (not @searchTopics))}} + +{{else if (not @loading)}} +
+ {{#if @suggestionKeyword}} + + {{else if this.termTooShort}} +
{{i18n "search.too_short"}}
+ {{else if this.noTopicResults}} +
{{i18n "search.no_results"}}
+ {{else if this.renderInitialOptions}} + {{else}} - {{#unless @inPMInboxContext}} - {{! render the first couple suggestions before a search has been performed}} - + {{#if @searchTopics}} + {{! render results after a search has been performed }} {{#if this.resultTypesWithComponent}} + {{/if}} - {{/unless}} + {{else}} + {{#unless @inPMInboxContext}} + {{! render the first couple suggestions before a search has been performed}} + + {{#if this.resultTypesWithComponent}} + + {{/if}} + {{/unless}} + {{/if}} {{/if}} - {{/if}} -
\ No newline at end of file +
+{{/if}} \ No newline at end of file diff --git a/app/assets/javascripts/discourse/app/components/search-menu/results/assistant-item.js b/app/assets/javascripts/discourse/app/components/search-menu/results/assistant-item.js index 4008f1a33a8..4c4e994ae10 100644 --- a/app/assets/javascripts/discourse/app/components/search-menu/results/assistant-item.js +++ b/app/assets/javascripts/discourse/app/components/search-menu/results/assistant-item.js @@ -3,10 +3,7 @@ import getURL from "discourse-common/lib/get-url"; import { inject as service } from "@ember/service"; import { action } from "@ember/object"; import { debounce } from "discourse-common/utils/decorators"; -import { - focusSearchButton, - focusSearchInput, -} from "discourse/components/search-menu"; +import { focusSearchInput } from "discourse/components/search-menu"; export default class AssistantItem extends Component { @service search; @@ -65,7 +62,6 @@ export default class AssistantItem extends Component { } if (e.key === "Escape") { - focusSearchButton(); this.args.closeSearchMenu(); e.preventDefault(); return false; diff --git a/app/assets/javascripts/discourse/app/components/search-menu/results/more-link.hbs b/app/assets/javascripts/discourse/app/components/search-menu/results/more-link.hbs index a035c876406..72cccafbc1d 100644 --- a/app/assets/javascripts/discourse/app/components/search-menu/results/more-link.hbs +++ b/app/assets/javascripts/discourse/app/components/search-menu/results/more-link.hbs @@ -2,7 +2,11 @@ {{! template-lint-disable no-invalid-interactive }}
{{#if this.moreUrl}} - + {{i18n "more"}}... {{else if this.topicResults.more}} diff --git a/app/assets/javascripts/discourse/app/components/search-menu/results/more-link.js b/app/assets/javascripts/discourse/app/components/search-menu/results/more-link.js index 994c1a946ef..22cbf8c353d 100644 --- a/app/assets/javascripts/discourse/app/components/search-menu/results/more-link.js +++ b/app/assets/javascripts/discourse/app/components/search-menu/results/more-link.js @@ -1,7 +1,7 @@ import Component from "@glimmer/component"; +import DiscourseURL from "discourse/lib/url"; import { action } from "@ember/object"; import { inject as service } from "@ember/service"; -import { focusSearchButton } from "discourse/components/search-menu"; export default class MoreLink extends Component { @service search; @@ -17,16 +17,24 @@ export default class MoreLink extends Component { return this.topicResults.moreUrl && this.topicResults.moreUrl(); } + @action + transitionToMoreUrl(event) { + event.preventDefault(); + this.args.closeSearchMenu(); + DiscourseURL.routeTo(this.moreUrl); + return false; + } + @action moreOfType(type) { this.args.updateTypeFilter(type); this.args.triggerSearch(); + this.args.closeSearchMenu(); } @action onKeyup(e) { if (e.key === "Escape") { - focusSearchButton(); this.args.closeSearchMenu(); e.preventDefault(); return false; diff --git a/app/assets/javascripts/discourse/app/components/search-menu/results/recent-searches.js b/app/assets/javascripts/discourse/app/components/search-menu/results/recent-searches.js index 2f5c77407b8..c0e2298a907 100644 --- a/app/assets/javascripts/discourse/app/components/search-menu/results/recent-searches.js +++ b/app/assets/javascripts/discourse/app/components/search-menu/results/recent-searches.js @@ -2,7 +2,6 @@ import Component from "@glimmer/component"; import { inject as service } from "@ember/service"; import User from "discourse/models/user"; import { action } from "@ember/object"; -import { focusSearchButton } from "discourse/components/search-menu"; export default class RecentSearches extends Component { @service currentUser; @@ -32,7 +31,6 @@ export default class RecentSearches extends Component { @action onKeyup(e) { if (e.key === "Escape") { - focusSearchButton(); this.args.closeSearchMenu(); e.preventDefault(); return false; diff --git a/app/assets/javascripts/discourse/app/components/search-menu/results/types.hbs b/app/assets/javascripts/discourse/app/components/search-menu/results/types.hbs index aaf5d794e6d..bffb84f02b0 100644 --- a/app/assets/javascripts/discourse/app/components/search-menu/results/types.hbs +++ b/app/assets/javascripts/discourse/app/components/search-menu/results/types.hbs @@ -8,7 +8,11 @@ {{! template-lint-disable no-pointer-down-event-binding }} {{! template-lint-disable no-invalid-interactive }}
  • - +
  • diff --git a/app/assets/javascripts/discourse/app/components/search-menu/results/types.js b/app/assets/javascripts/discourse/app/components/search-menu/results/types.js index 6f53640045e..2e7199ea0fc 100644 --- a/app/assets/javascripts/discourse/app/components/search-menu/results/types.js +++ b/app/assets/javascripts/discourse/app/components/search-menu/results/types.js @@ -1,7 +1,6 @@ import Component from "@glimmer/component"; import { inject as service } from "@ember/service"; import { action } from "@ember/object"; -import { focusSearchButton } from "discourse/components/search-menu"; export default class Types extends Component { @service search; @@ -20,10 +19,14 @@ export default class Types extends Component { ); } + @action + onClick() { + this.args.closeSearchMenu(); + } + @action onKeydown(e) { if (e.key === "Escape") { - focusSearchButton(); this.args.closeSearchMenu(); e.preventDefault(); return false; diff --git a/app/assets/javascripts/discourse/app/components/search-menu/search-term.hbs b/app/assets/javascripts/discourse/app/components/search-menu/search-term.hbs index a00a0dd1e8a..c8fd2185fd1 100644 --- a/app/assets/javascripts/discourse/app/components/search-menu/search-term.hbs +++ b/app/assets/javascripts/discourse/app/components/search-menu/search-term.hbs @@ -7,5 +7,6 @@ aria-label={{i18n "search.title"}} {{on "keyup" this.onKeyup}} {{on "input" this.updateSearchTerm}} + {{on "focus" @openSearchMenu}} {{did-insert this.focus}} /> \ No newline at end of file diff --git a/app/assets/javascripts/discourse/app/components/search-menu/search-term.js b/app/assets/javascripts/discourse/app/components/search-menu/search-term.js index 4b78f0c8cd2..062dc7fab5d 100644 --- a/app/assets/javascripts/discourse/app/components/search-menu/search-term.js +++ b/app/assets/javascripts/discourse/app/components/search-menu/search-term.js @@ -6,7 +6,6 @@ import { inject as service } from "@ember/service"; import { DEFAULT_TYPE_FILTER, SEARCH_INPUT_ID, - focusSearchButton, } from "discourse/components/search-menu"; const SECOND_ENTER_MAX_DELAY = 15000; @@ -35,19 +34,22 @@ export default class SearchTerm extends Component { @action focus(element) { - element.focus(); - element.select(); + if (this.args.autofocus) { + element.focus(); + element.select(); + } } @action onKeyup(e) { if (e.key === "Escape") { - focusSearchButton(); this.args.closeSearchMenu(); e.preventDefault(); return false; } + this.args.openSearchMenu(); + this.search.handleArrowUpOrDown(e); if (e.key === "Enter") { diff --git a/app/assets/javascripts/discourse/app/widgets/header.js b/app/assets/javascripts/discourse/app/widgets/header.js index 95453ad58e8..1470a8256ba 100644 --- a/app/assets/javascripts/discourse/app/widgets/header.js +++ b/app/assets/javascripts/discourse/app/widgets/header.js @@ -12,7 +12,8 @@ import { wantsNewWindow } from "discourse/lib/intercept-click"; import { logSearchLinkClick } from "discourse/lib/search"; import RenderGlimmer from "discourse/widgets/render-glimmer"; import { hbs } from "ember-cli-htmlbars"; -import { SEARCH_BUTTON_ID } from "discourse/components/search-menu"; + +const SEARCH_BUTTON_ID = "search-button"; let _extraHeaderIcons = []; @@ -393,14 +394,17 @@ createWidget("glimmer-search-menu-wrapper", { new RenderGlimmer( this, "div.widget-component-connector", - hbs``, - { closeSearchMenu: this.closeSearchMenu.bind(this) } + hbs``, + { + closeSearchMenu: this.closeSearchMenu.bind(this), + } ), ]; }, closeSearchMenu() { this.sendWidgetAction("toggleSearchMenu"); + document.getElementById(SEARCH_BUTTON_ID)?.focus(); }, clickOutside() { 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 new file mode 100644 index 00000000000..5bac77fa657 --- /dev/null +++ b/app/assets/javascripts/discourse/tests/integration/components/search-menu-test.gjs @@ -0,0 +1,58 @@ +import I18n from "I18n"; +import SearchMenu from "discourse/components/search-menu"; +import { module, test } from "qunit"; +import { setupRenderingTest } from "discourse/tests/helpers/component-test"; +import { click, fillIn, render, triggerKeyEvent } from "@ember/test-helpers"; +import { exists, query } from "discourse/tests/helpers/qunit-helpers"; + +// Note this isn't a full-fledge test of the search menu. Those tests are in +// acceptance/glimmer-search-test.js. This is simply about the rendering of the +// menu panel separate from the search input. +module("Integration | Component | search-menu", function (hooks) { + setupRenderingTest(hooks); + + test("rendering standalone", async function (assert) { + await render(); + + assert.ok( + exists(".show-advanced-search"), + "it shows full page search button" + ); + + assert.notOk(exists(".menu-panel"), "Menu panel is not rendered yet"); + + await click("#search-term"); + + assert.ok( + exists(".menu-panel .search-menu-initial-options"), + "Menu panel is rendered with initial options" + ); + + await fillIn("#search-term", "test"); + + assert.strictEqual( + query(".label-suffix").textContent.trim(), + I18n.t("search.in_topics_posts"), + "search label reflects context of search" + ); + + await triggerKeyEvent("#search-term", "keyup", "Enter"); + + assert.ok( + exists(".search-result-topic"), + "search result is a list of topics" + ); + + await triggerKeyEvent("#search-term", "keyup", "Escape"); + + assert.notOk(exists(".menu-panel"), "Menu panel is gone"); + + await click("#search-term"); + await click("#search-term"); + + assert.ok( + exists(".search-result-topic"), + "Clicking the term brought back search results" + ); + }); +}); diff --git a/app/assets/stylesheets/common/base/search-menu.scss b/app/assets/stylesheets/common/base/search-menu.scss index 83787d9ffc1..a198af92db9 100644 --- a/app/assets/stylesheets/common/base/search-menu.scss +++ b/app/assets/stylesheets/common/base/search-menu.scss @@ -14,7 +14,8 @@ $search-pad-vertical: 0.25em; $search-pad-horizontal: 0.5em; -.search-menu { +.search-menu, +.search-menu-container { .menu-panel .panel-body-contents { overflow-y: auto; } @@ -60,6 +61,18 @@ $search-pad-horizontal: 0.5em; margin-right: 0px; } + &.menu-panel-results { + position: relative; + + .menu-panel { + position: absolute; + left: 0; + right: 0; + top: unset; + width: unset; + } + } + .results { display: flex; flex-direction: column;