diff --git a/app/assets/javascripts/discourse/widgets/search-menu-results.js.es6 b/app/assets/javascripts/discourse/widgets/search-menu-results.js.es6 index c195c962f6b..767f192c871 100644 --- a/app/assets/javascripts/discourse/widgets/search-menu-results.js.es6 +++ b/app/assets/javascripts/discourse/widgets/search-menu-results.js.es6 @@ -20,13 +20,18 @@ class Highlighted extends RawHtml { function createSearchResult({ type, linkField, builder }) { return createWidget(`search-result-${type}`, { html(attrs) { - return attrs.results.map(r => { + let i=-1; + + return attrs.results.map(r => { + i+=1; let searchResultId; if (type === "topic") { searchResultId = r.get('topic_id'); } - return h('li', this.attach('link', { + let className = i === attrs.selected ? '.selected' : ''; + + return h('li' + className, { attributes: { tabindex: '-1' } }, this.attach('link', { href: r.get(linkField), contents: () => builder.call(this, r, attrs.term), className: 'search-link', @@ -126,7 +131,8 @@ createWidget('search-menu-results', { searchContextEnabled: attrs.searchContextEnabled, searchLogId: attrs.results.grouped_search_result.search_log_id, results: rt.results, - term: attrs.term + term: attrs.term, + selected: (attrs.selected && attrs.selected.type === rt.type) ? attrs.selected.index : -1 })), h('div.no-results', more) ]; diff --git a/app/assets/javascripts/discourse/widgets/search-menu.js.es6 b/app/assets/javascripts/discourse/widgets/search-menu.js.es6 index d5e9b12e9ac..08aa8f1b115 100644 --- a/app/assets/javascripts/discourse/widgets/search-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/search-menu.js.es6 @@ -9,7 +9,8 @@ const searchData = { noResults: false, term: undefined, typeFilter: null, - invalidTerm: false + invalidTerm: false, + selected: null }; // Helps with debouncing and cancelling promises @@ -132,7 +133,8 @@ export default createWidget('search-menu', { noResults: searchData.noResults, results: searchData.results, invalidTerm: searchData.invalidTerm, - searchContextEnabled: searchData.contextEnabled + searchContextEnabled: searchData.contextEnabled, + selected: searchData.selected })); } } @@ -169,6 +171,81 @@ export default createWidget('search-menu', { this.sendWidgetAction('toggleSearchMenu'); }, + keyDown(e) { + if (searchData.loading || searchData.noResults) { + return; + } + + if (e.which === 13 /*enter*/ && searchData.selected) { + searchData.selected = null; + $('header .results li.selected a').click(); + } + + if (e.which === 38 /*arrow up*/ || e.which === 40 /*arrow down*/) { + this.moveSelected(e.which === 38 ? -1 : 1); + + this.scheduleRerender(); + + Em.run.next(()=>{ + if (searchData.selected) { + + // so we do not clear selected + $('header .results li').off('blur'); + + let selected = $('header .results li.selected') + .focus() + .on('blur', ()=> { + searchData.selected = null; + this.scheduleRerender(); + selected.off('blur'); + }); + + } else { + $('#search-term').focus(); + } + }); + + e.preventDefault(); + return false; + } + }, + + moveSelected(offset) { + + if (offset === 1 && !searchData.selected) { + searchData.selected = {type: searchData.results.resultTypes[0].type, index: 0}; + return; + } + + if (!searchData.selected) { + return; + } + + let typeIndex = _.findIndex(searchData.results.resultTypes, item => item.type === searchData.selected.type); + + if (typeIndex === 0 && searchData.selected.index === 0 && offset === -1) { + searchData.selected = null; + return; + } + + let currentResults = searchData.results.resultTypes[typeIndex].results; + let newPosition = searchData.selected.index + offset; + + if (newPosition < currentResults.length && newPosition >= 0) { + searchData.selected.index = newPosition; + } else { + // possibly move to next type + let newTypeIndex = typeIndex + offset; + if (newTypeIndex >= 0 && newTypeIndex < searchData.results.resultTypes.length) { + newPosition = 0; + if (offset === -1) { + newPosition = searchData.results.resultTypes[newTypeIndex].results.length - 1; + } + searchData.selected = {type: searchData.results.resultTypes[newTypeIndex].type, index: newPosition}; + } + } + }, + triggerSearch() { searchData.noResults = false; this.searchService().set('highlightTerm', searchData.term); diff --git a/app/assets/stylesheets/common/base/menu-panel.scss b/app/assets/stylesheets/common/base/menu-panel.scss index 21e80fb44e6..09dde7b8d0f 100644 --- a/app/assets/stylesheets/common/base/menu-panel.scss +++ b/app/assets/stylesheets/common/base/menu-panel.scss @@ -18,7 +18,7 @@ } .menu-panel { - border: 1px solid $primary-low; + border: 1px solid $primary-low; box-shadow: 0 2px 2px rgba(0,0,0, .25); background-color: $secondary; z-index: 1100; @@ -148,6 +148,13 @@ padding: 5px; text-align: center; } + + li.selected { + background-color: $highlight-medium; + } + li:focus { + outline: none; + } .filter { padding: 0; &:hover {background: transparent;}