diff --git a/app/common/lib/suggestion.js b/app/common/lib/suggestion.js index b9089ac53cd..651419c0c00 100644 --- a/app/common/lib/suggestion.js +++ b/app/common/lib/suggestion.js @@ -602,6 +602,52 @@ const generateNewSearchXHRResults = debounce((state, windowId, tabId, input) => } }, 10) +const filterSuggestionListByType = (suggestionList) => { + const bookmarkSuggestions = [] + const historySuggestions = [] + const aboutPagesSuggestions = [] + const tabSuggestions = [] + const searchSuggestions = [] + const topSiteSuggestions = [] + + suggestionList.forEach(item => { + switch (item.get('type')) { + case suggestionTypes.BOOKMARK: + bookmarkSuggestions.push(item) + break + + case suggestionTypes.HISTORY: + historySuggestions.push(item) + break + + case suggestionTypes.ABOUT_PAGES: + aboutPagesSuggestions.push(item) + break + + case suggestionTypes.TAB: + tabSuggestions.push(item) + break + + case suggestionTypes.SEARCH: + searchSuggestions.push(item) + break + + case suggestionTypes.TOP_SITE: + topSiteSuggestions.push(item) + break + } + }) + + return { + bookmarkSuggestions, + historySuggestions, + aboutPagesSuggestions, + tabSuggestions, + searchSuggestions, + topSiteSuggestions + } +} + module.exports = { sortingPriority, sortByAccessCountWithAgeDecay, @@ -621,5 +667,6 @@ module.exports = { getSearchSuggestions, getAlexaSuggestions, generateNewSuggestionsList, - generateNewSearchXHRResults + generateNewSearchXHRResults, + filterSuggestionListByType } diff --git a/app/renderer/components/navigation/urlBar.js b/app/renderer/components/navigation/urlBar.js index c63365b65b9..fa798f96112 100644 --- a/app/renderer/components/navigation/urlBar.js +++ b/app/renderer/components/navigation/urlBar.js @@ -615,10 +615,8 @@ class UrlBar extends React.Component { { this.shouldRenderUrlBarSuggestions ? + menubarVisible={this.props.menubarVisible} + /> : null } diff --git a/app/renderer/components/navigation/urlBarSuggestionItem.js b/app/renderer/components/navigation/urlBarSuggestionItem.js index 73400bf7f73..c0fb3c153dd 100644 --- a/app/renderer/components/navigation/urlBarSuggestionItem.js +++ b/app/renderer/components/navigation/urlBarSuggestionItem.js @@ -3,24 +3,57 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ const React = require('react') + +// Components const ImmutableComponent = require('../immutableComponent') + +// Constants const suggestionTypes = require('../../../../js/constants/suggestionTypes') + +// Actions +const appActions = require('../../../../js/actions/appActions') +const windowActions = require('../../../../js/actions/windowActions') + +// utils const cx = require('../../../../js/lib/classSet') +const {isForSecondaryAction} = require('../../../../js/lib/eventUtil') +const {getCurrentWindowId} = require('../../currentWindow') class UrlBarSuggestionItem extends ImmutableComponent { + constructor () { + super() + this.onMouseOver = this.onMouseOver.bind(this) + } + + onMouseOver (e) { + let newIndex = parseInt(e.target.getAttribute('data-index'), 10) + + if (newIndex < 0) { + newIndex = null + } + + appActions.urlBarSelectedIndexChanged(getCurrentWindowId(), newIndex) + } + + onClick (e) { + windowActions.activeSuggestionClicked(isForSecondaryAction(e), e.shiftKey) + } + componentDidMount () { - this.node.addEventListener('auxclick', this.props.onClick) + this.node.addEventListener('auxclick', this.onClick) } + componentWillUpdate (nextProps) { if (!this.props.selected && nextProps.selected) { this.node.scrollIntoView() } } + render () { return
  • { this.node = node }} className={cx({ diff --git a/app/renderer/components/navigation/urlBarSuggestions.js b/app/renderer/components/navigation/urlBarSuggestions.js index 3295dba0d5a..b52e4a7a0de 100644 --- a/app/renderer/components/navigation/urlBarSuggestions.js +++ b/app/renderer/components/navigation/urlBarSuggestions.js @@ -3,112 +3,103 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ const React = require('react') -const ImmutableComponent = require('../immutableComponent') +const Immutable = require('immutable') +// Components +const ReduxComponent = require('../reduxComponent') const UrlBarSuggestionItem = require('./urlBarSuggestionItem') -const windowActions = require('../../../../js/actions/windowActions') + +// Actions const appActions = require('../../../../js/actions/appActions') -const suggestionTypes = require('../../../../js/constants/suggestionTypes') + +// Utils const cx = require('../../../../js/lib/classSet') const locale = require('../../../../js/l10n') -const {isForSecondaryAction} = require('../../../../js/lib/eventUtil') +const suggestions = require('../../../common/lib/suggestion') +const frameStateUtil = require('../../../../js/state/frameStateUtil') const {getCurrentWindowId} = require('../../currentWindow') -class UrlBarSuggestions extends ImmutableComponent { - constructor () { - super() - this.onSuggestionClicked = this.onSuggestionClicked.bind(this) - this.onMouseOver = this.onMouseOver.bind(this) - } - - get activeIndex () { - if (this.props.suggestionList === null) { - return -1 - } - return this.props.selectedIndex - } - +class UrlBarSuggestions extends React.Component { blur () { appActions.urlBarSuggestionsChanged(getCurrentWindowId(), null, null) } - onSuggestionClicked (e) { - windowActions.activeSuggestionClicked(isForSecondaryAction(e), e.shiftKey) - } - - render () { - const suggestions = this.props.suggestionList - const bookmarkSuggestions = suggestions.filter((s) => s.get('type') === suggestionTypes.BOOKMARK) - const historySuggestions = suggestions.filter((s) => s.get('type') === suggestionTypes.HISTORY) - const aboutPagesSuggestions = suggestions.filter((s) => s.get('type') === suggestionTypes.ABOUT_PAGES) - const tabSuggestions = suggestions.filter((s) => s.get('type') === suggestionTypes.TAB) - const searchSuggestions = suggestions.filter((s) => s.get('type') === suggestionTypes.SEARCH) - const topSiteSuggestions = suggestions.filter((s) => s.get('type') === suggestionTypes.TOP_SITE) - + generateAllItems () { let items = [] let index = 0 + const addToItems = (suggestions, sectionKey, title, icon) => { - if (suggestions.size > 0) { + if (suggestions.length > 0) { items.push(
  • { icon - ? - : null + ? + : null } {title}
  • ) } + items = items.concat(suggestions.map((suggestion, i) => { const currentIndex = index + i - const selected = this.activeIndex === currentIndex || (!this.activeIndex && currentIndex === 0 && this.props.hasSuggestionMatch) + const selected = this.props.activeIndex === currentIndex || (this.props.activeIndex == null && currentIndex === 0 && this.props.hasSuggestionMatch) return + i={i} /> })) - index += suggestions.size + index += suggestions.length } - addToItems(historySuggestions, 'historyTitle', locale.translation('historySuggestionTitle'), 'fa-clock-o') - addToItems(bookmarkSuggestions, 'bookmarksTitle', locale.translation('bookmarksSuggestionTitle'), 'fa-star-o') - addToItems(aboutPagesSuggestions, 'aboutPagesTitle', locale.translation('aboutPagesSuggestionTitle'), null) - addToItems(tabSuggestions, 'tabsTitle', locale.translation('tabsSuggestionTitle'), 'fa-external-link') - addToItems(searchSuggestions, 'searchTitle', locale.translation('searchSuggestionTitle'), 'fa-search') - addToItems(topSiteSuggestions, 'topSiteTitle', locale.translation('topSiteSuggestionTitle'), 'fa-link') - const documentHeight = Number.parseInt(window.getComputedStyle(document.querySelector(':root')).getPropertyValue('--navbar-height'), 10) - const menuHeight = this.props.menubarVisible ? 30 : 0 - return - } - onMouseOver (e) { - this.updateSuggestions(parseInt(e.target.dataset.index, 10)) + const list = suggestions.filterSuggestionListByType(this.props.suggestionList) + + addToItems(list.historySuggestions, 'historyTitle', locale.translation('historySuggestionTitle'), 'fa-clock-o') + addToItems(list.bookmarkSuggestions, 'bookmarksTitle', locale.translation('bookmarksSuggestionTitle'), 'fa-star-o') + addToItems(list.aboutPagesSuggestions, 'aboutPagesTitle', locale.translation('aboutPagesSuggestionTitle'), null) + addToItems(list.tabSuggestions, 'tabsTitle', locale.translation('tabsSuggestionTitle'), 'fa-external-link') + addToItems(list.searchSuggestions, 'searchTitle', locale.translation('searchSuggestionTitle'), 'fa-search') + addToItems(list.topSiteSuggestions, 'topSiteTitle', locale.translation('topSiteSuggestionTitle'), 'fa-link') + + return items } - componentDidMount () { + mergeProps (state, dispatchProps, ownProps) { + const currentWindow = state.get('currentWindow') + const activeFrame = frameStateUtil.getActiveFrame(currentWindow) || Immutable.Map() + const urlBar = activeFrame.getIn(['navbar', 'urlbar']) || Immutable.Map() + const documentHeight = Number.parseInt( + window.getComputedStyle(document.querySelector(':root')).getPropertyValue('--navbar-height'), 10 + ) + const menuHeight = ownProps.menubarVisible ? 30 : 0 + + const props = {} + // used in renderer + props.maxHeight = document.documentElement.offsetHeight - documentHeight - 2 - menuHeight + + // used in functions + props.menubarVisible = ownProps.menubarVisible + props.suggestionList = urlBar.getIn(['suggestions', 'suggestionList']) // TODO (nejc) improve, use primitives + props.hasSuggestionMatch = urlBar.getIn(['suggestions', 'hasSuggestionMatch']) + props.activeIndex = props.suggestionList === null + ? -1 + : urlBar.getIn(['suggestions', 'selectedIndex']) + + return props } - updateSuggestions (newIndex) { - const suggestions = this.suggestionList || this.props.suggestionList - if (!suggestions) { - return - } - // Update the urlbar preview content - if (newIndex > suggestions.size) { - newIndex = null - } - appActions.urlBarSuggestionsChanged(getCurrentWindowId(), suggestions, newIndex) + render () { + return } } -module.exports = UrlBarSuggestions +module.exports = ReduxComponent.connect(UrlBarSuggestions) diff --git a/app/renderer/reducers/urlBarReducer.js b/app/renderer/reducers/urlBarReducer.js index 98d1940b5a0..556aa62465e 100644 --- a/app/renderer/reducers/urlBarReducer.js +++ b/app/renderer/reducers/urlBarReducer.js @@ -156,6 +156,13 @@ const urlBarReducer = (state, action) => { case appConstants.APP_URL_BAR_TEXT_CHANGED: state = setNavBarUserInput(state, action.input) break + case appConstants.APP_URL_BAR_SELECTED_INDEX_CHANGED: + { + const suggestionList = state.getIn(activeFrameStatePath(state).concat(['navbar', 'urlbar', 'suggestions', 'suggestionList'])) + state = state.setIn(activeFrameStatePath(state).concat(['navbar', 'urlbar', 'suggestions', 'selectedIndex']), action.selectedIndex) + state = updateUrlSuffix(state, suggestionList) + break + } case appConstants.APP_URL_BAR_SUGGESTIONS_CHANGED: if (action.selectedIndex !== undefined) { state = state.setIn(activeFrameStatePath(state).concat(['navbar', 'urlbar', 'suggestions', 'selectedIndex']), action.selectedIndex) diff --git a/docs/appActions.md b/docs/appActions.md index f5f8fa5bd23..878ae18c534 100644 --- a/docs/appActions.md +++ b/docs/appActions.md @@ -1098,6 +1098,18 @@ Indicates URL bar suggestions and selected index. +### urlBarSelectedIndexChanged(windowId, selectedIndex) + +Indicates URL bar selected index + +**Parameters** + +**windowId**: `number`, the window ID + +**selectedIndex**: `number`, The index for the selected item (users can select items with down arrow on their keyboard) + + + ### defaultSearchEngineLoaded(searchDetail) Dispatches a message to set the search engine details. diff --git a/js/actions/appActions.js b/js/actions/appActions.js index 361a60ab69c..2d18741b5da 100644 --- a/js/actions/appActions.js +++ b/js/actions/appActions.js @@ -1393,6 +1393,23 @@ const appActions = { }) }, + /** + * Indicates URL bar selected index + * + * @param {number} windowId - the window ID + * @param {number} selectedIndex - The index for the selected item (users can select items with down arrow on their keyboard) + */ + urlBarSelectedIndexChanged: function (windowId, selectedIndex) { + dispatch({ + actionType: appConstants.APP_URL_BAR_SELECTED_INDEX_CHANGED, + selectedIndex, + windowId, + queryInfo: { + windowId + } + }) + }, + /** * Dispatches a message to set the search engine details. * @param {Object} searchDetail - the search details diff --git a/js/constants/appConstants.js b/js/constants/appConstants.js index f1c518e3932..b517cf7e2be 100644 --- a/js/constants/appConstants.js +++ b/js/constants/appConstants.js @@ -130,7 +130,8 @@ const appConstants = { APP_ADD_PASSWORD: _, /** @param {Object} passwordDetail */ APP_REMOVE_PASSWORD: _, /** @param {Object} passwordDetail */ APP_REMOVE_PASSWORD_SITE: _, /** @param {Object} passwordDetail */ - APP_CLEAR_PASSWORDS: _ + APP_CLEAR_PASSWORDS: _, + APP_URL_BAR_SELECTED_INDEX_CHANGED: _ } module.exports = mapValuesByKeys(appConstants) diff --git a/test/unit/app/renderer/components/navigation/urlBarSuggestionItemTest.js b/test/unit/app/renderer/components/navigation/urlBarSuggestionItemTest.js index d7ecf4616ce..f8fb265a2ee 100644 --- a/test/unit/app/renderer/components/navigation/urlBarSuggestionItemTest.js +++ b/test/unit/app/renderer/components/navigation/urlBarSuggestionItemTest.js @@ -1,7 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ -/* global describe, before, after, it */ +/* global describe, before, after, it, afterEach */ const mockery = require('mockery') const {mount} = require('enzyme') @@ -10,10 +10,11 @@ const sinon = require('sinon') const Immutable = require('immutable') const fakeElectron = require('../../../../lib/fakeElectron') const suggestionTypes = require('../../../../../../js/constants/suggestionTypes') -let UrlBarSuggestionItem require('../../../../braveUnit') describe('UrlBarSuggestionItem component', function () { + let appActions, windowActions, UrlBarSuggestionItem + before(function () { mockery.enable({ warnOnReplace: false, @@ -22,7 +23,17 @@ describe('UrlBarSuggestionItem component', function () { }) mockery.registerMock('electron', fakeElectron) UrlBarSuggestionItem = require('../../../../../../app/renderer/components/navigation/urlBarSuggestionItem') + appActions = require('../../../../../../js/actions/appActions') + windowActions = require('../../../../../../js/actions/windowActions') + this.onMouseOver = sinon.spy(appActions, 'urlBarSelectedIndexChanged') + this.onSuggestionClicked = sinon.spy(windowActions, 'activeSuggestionClicked') + }) + + afterEach(function () { + this.onMouseOver.reset() + this.onSuggestionClicked.reset() }) + after(function () { mockery.disable() }) @@ -30,8 +41,6 @@ describe('UrlBarSuggestionItem component', function () { Object.values(suggestionTypes).forEach((suggestionType) => { describe(`${suggestionType} suggestion item`, function () { before(function () { - this.onMouseOver = sinon.spy() - this.onSuggestionClicked = sinon.spy() this.suggestion = Immutable.fromJS({ title: 'hello', type: suggestionType, @@ -42,8 +51,6 @@ describe('UrlBarSuggestionItem component', function () { selected currentIndex={1} i={0} - onMouseOver={this.onMouseOver} - onClick={this.onSuggestionClicked} />) }) @@ -69,13 +76,13 @@ describe('UrlBarSuggestionItem component', function () { } }) - it('detects mouse moves', function () { + it('detects mouse click', function () { this.result.simulate('click') assert.ok(this.onSuggestionClicked.calledOnce) assert.ok(this.onMouseOver.notCalled) }) - it('detects mouse moves', function () { + it('detects mouse over', function () { this.result.simulate('mouseover') assert.ok(this.onMouseOver.calledOnce) })