From e5d3803061f01e5980c8bbe060ef4d73486a630d Mon Sep 17 00:00:00 2001 From: fenglish Date: Mon, 12 Nov 2018 12:15:13 +0000 Subject: [PATCH 01/71] first commit --- components/x-topic-search/.npmignore | 3 ++ components/x-topic-search/package.json | 35 +++++++++++++++ components/x-topic-search/readme.md | 43 +++++++++++++++++++ components/x-topic-search/rollup.js | 4 ++ components/x-topic-search/src/TopicSearch.jsx | 10 +++++ components/x-topic-search/stories/example.js | 9 ++++ components/x-topic-search/stories/index.js | 15 +++++++ 7 files changed, 119 insertions(+) create mode 100644 components/x-topic-search/.npmignore create mode 100644 components/x-topic-search/package.json create mode 100644 components/x-topic-search/readme.md create mode 100644 components/x-topic-search/rollup.js create mode 100644 components/x-topic-search/src/TopicSearch.jsx create mode 100644 components/x-topic-search/stories/example.js create mode 100644 components/x-topic-search/stories/index.js diff --git a/components/x-topic-search/.npmignore b/components/x-topic-search/.npmignore new file mode 100644 index 000000000..a44a9e753 --- /dev/null +++ b/components/x-topic-search/.npmignore @@ -0,0 +1,3 @@ +src/ +stories/ +rollup.js diff --git a/components/x-topic-search/package.json b/components/x-topic-search/package.json new file mode 100644 index 000000000..d6ae3ef57 --- /dev/null +++ b/components/x-topic-search/package.json @@ -0,0 +1,35 @@ +{ + "name": "@financial-times/x-topic-search", + "version": "0.0.0", + "description": "", + "main": "dist/TopicSearch.cjs.js", + "module": "dist/TopicSearch.esm.js", + "browser": "dist/TopicSearch.es5.js", + "scripts": { + "prepare": "npm run build", + "build": "node rollup.js", + "start": "node rollup.js --watch" + }, + "keywords": [ + "x-dash" + ], + "author": "", + "license": "ISC", + "dependencies": { + "@financial-times/x-engine": "file:../../packages/x-engine" + }, + "devDependencies": { + "@financial-times/x-rollup": "file:../../packages/x-rollup" + }, + "repository": { + "type": "git", + "url": "https://github.com/Financial-Times/x-dash.git" + }, + "homepage": "https://github.com/Financial-Times/x-dash/tree/master/components/x-topic-search", + "engines": { + "node": ">= 6.0.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/components/x-topic-search/readme.md b/components/x-topic-search/readme.md new file mode 100644 index 000000000..bce365210 --- /dev/null +++ b/components/x-topic-search/readme.md @@ -0,0 +1,43 @@ +# x-topic-search + +This module has these features and scope. + + +## Installation + +This module is compatible with Node 6+ and is distributed on npm. + +```bash +npm install --save @financial-times/x-topic-search +``` + +The [`x-engine`][engine] module is used to inject your chosen runtime into the component. Please read the `x-engine` documentation first if you are consuming `x-` components for the first time in your application. + +[engine]: https://github.com/Financial-Times/x-dash/tree/master/packages/x-engine + + +## Usage + +The components provided by this module are all functions that expect a map of [properties](#properties). They can be used with vanilla JavaScript or JSX (If you are not familiar check out [WTF is JSX][jsx-wtf] first). For example if you were writing your application using React you could use the component like this: + +```jsx +import React from 'react'; +import { TopicSearch } from '@financial-times/x-topic-search'; + +// A == B == C +const a = TopicSearch(props); +const b = ; +const c = React.createElement(TopicSearch, props); +``` + +All `x-` components are designed to be compatible with a variety of runtimes, not just React. Check out the [`x-engine`][engine] documentation for a list of recommended libraries and frameworks. + +[jsx-wtf]: https://jasonformat.com/wtf-is-jsx/ + +### Properties + +Feature | Type | Notes +-----------------|--------|---------------------------- +`propertyName1` | String | +`propertyName2` | String | +`propertyName2` | String | diff --git a/components/x-topic-search/rollup.js b/components/x-topic-search/rollup.js new file mode 100644 index 000000000..2196bfba7 --- /dev/null +++ b/components/x-topic-search/rollup.js @@ -0,0 +1,4 @@ +const xRollup = require('@financial-times/x-rollup'); +const pkg = require('./package.json'); + +xRollup({ input: './src/TopicSearch.jsx', pkg }); diff --git a/components/x-topic-search/src/TopicSearch.jsx b/components/x-topic-search/src/TopicSearch.jsx new file mode 100644 index 000000000..932a3f4fc --- /dev/null +++ b/components/x-topic-search/src/TopicSearch.jsx @@ -0,0 +1,10 @@ +import { h } from '@financial-times/x-engine'; + +const TopicSearch = (props) => ( +
+

Welcome to x-topic-search

+

{props.message}

+
+); + +export { TopicSearch }; diff --git a/components/x-topic-search/stories/example.js b/components/x-topic-search/stories/example.js new file mode 100644 index 000000000..6df941bf7 --- /dev/null +++ b/components/x-topic-search/stories/example.js @@ -0,0 +1,9 @@ +exports.title = 'Example'; + +exports.data = { + message: 'Hello World!' +}; + +// This reference is only required for hot module loading in development +// +exports.m = module; diff --git a/components/x-topic-search/stories/index.js b/components/x-topic-search/stories/index.js new file mode 100644 index 000000000..26239e05c --- /dev/null +++ b/components/x-topic-search/stories/index.js @@ -0,0 +1,15 @@ +const { TopicSearch } = require('../'); + +exports.component = TopicSearch; + +exports.package = require('../package.json'); + +// Set up basic document styling using the Origami build service +exports.dependencies = { + 'o-normalise': '^1.6.0', + 'o-typography': '^5.5.0' +}; + +exports.stories = [ + require('./example') +]; From f1cba1c2eb47d17d89e71e2552086429db975347 Mon Sep 17 00:00:00 2001 From: fenglish Date: Mon, 12 Nov 2018 16:47:26 +0000 Subject: [PATCH 02/71] apply existing functions from ft.com topic search bar --- components/x-topic-search/.bowerrc | 8 ++ components/x-topic-search/bower.json | 11 ++ components/x-topic-search/package.json | 10 +- components/x-topic-search/readme.md | 13 ++- .../x-topic-search/src/NoSuggestions.jsx | 22 ++++ .../x-topic-search/src/ResultContainer.jsx | 46 ++++++++ .../x-topic-search/src/SuggestionList.jsx | 22 ++++ components/x-topic-search/src/TopicSearch.jsx | 77 ++++++++++++- .../x-topic-search/src/TopicSearch.scss | 106 ++++++++++++++++++ .../x-topic-search/src/lib/get-suggestions.js | 52 +++++++++ components/x-topic-search/stories/example.js | 9 -- components/x-topic-search/stories/index.js | 6 +- .../x-topic-search/stories/topic-search.js | 17 +++ 13 files changed, 375 insertions(+), 24 deletions(-) create mode 100644 components/x-topic-search/.bowerrc create mode 100644 components/x-topic-search/bower.json create mode 100644 components/x-topic-search/src/NoSuggestions.jsx create mode 100644 components/x-topic-search/src/ResultContainer.jsx create mode 100644 components/x-topic-search/src/SuggestionList.jsx create mode 100644 components/x-topic-search/src/TopicSearch.scss create mode 100644 components/x-topic-search/src/lib/get-suggestions.js delete mode 100644 components/x-topic-search/stories/example.js create mode 100644 components/x-topic-search/stories/topic-search.js diff --git a/components/x-topic-search/.bowerrc b/components/x-topic-search/.bowerrc new file mode 100644 index 000000000..39039a4a1 --- /dev/null +++ b/components/x-topic-search/.bowerrc @@ -0,0 +1,8 @@ +{ + "registry": { + "search": [ + "https://origami-bower-registry.ft.com", + "https://registry.bower.io" + ] + } +} diff --git a/components/x-topic-search/bower.json b/components/x-topic-search/bower.json new file mode 100644 index 000000000..1837995b5 --- /dev/null +++ b/components/x-topic-search/bower.json @@ -0,0 +1,11 @@ +{ + "name": "x-topic-search", + "main": "dist/TopicSearch.es5.js", + "private": true, + "dependencies": { + "o-icons": "^5.8.0", + "o-forms": "^5.9.0", + "o-typography": "^5.7.8", + "o-colors": "^4.7.7" + } +} diff --git a/components/x-topic-search/package.json b/components/x-topic-search/package.json index d6ae3ef57..19defd952 100644 --- a/components/x-topic-search/package.json +++ b/components/x-topic-search/package.json @@ -5,6 +5,7 @@ "main": "dist/TopicSearch.cjs.js", "module": "dist/TopicSearch.esm.js", "browser": "dist/TopicSearch.es5.js", + "style": "dist/TopicSearch.css", "scripts": { "prepare": "npm run build", "build": "node rollup.js", @@ -16,10 +17,15 @@ "author": "", "license": "ISC", "dependencies": { - "@financial-times/x-engine": "file:../../packages/x-engine" + "@financial-times/x-engine": "file:../../packages/x-engine", + "@financial-times/x-interaction": "^1.0.0-beta.6", + "classnames": "^2.2.6", + "debounce-promise": "^3.1.0" }, "devDependencies": { - "@financial-times/x-rollup": "file:../../packages/x-rollup" + "@financial-times/x-rollup": "file:../../packages/x-rollup", + "node-sass": "^4.9.2", + "bower": "^1.7.9" }, "repository": { "type": "git", diff --git a/components/x-topic-search/readme.md b/components/x-topic-search/readme.md index bce365210..cf4c9c989 100644 --- a/components/x-topic-search/readme.md +++ b/components/x-topic-search/readme.md @@ -1,6 +1,6 @@ # x-topic-search -This module has these features and scope. +This module provides a topic search bar. ## Installation @@ -36,8 +36,9 @@ All `x-` components are designed to be compatible with a variety of runtimes, no ### Properties -Feature | Type | Notes ------------------|--------|---------------------------- -`propertyName1` | String | -`propertyName2` | String | -`propertyName2` | String | +Property | Type | Required | Note +------------------------|--------|----------|------------------ +`minSearchLength` | Number | No | Minimum chars to start search. Default is 2 +`maxSuggestions` | Number | No | Maximum number to display suggestions. Default is 5 +`apiUrl` | String | Yes | The url to use when making requests to get topics +`currentlyFollowingTopics` | Array | Yes | Each item should have `name` and `uuid` properties diff --git a/components/x-topic-search/src/NoSuggestions.jsx b/components/x-topic-search/src/NoSuggestions.jsx new file mode 100644 index 000000000..c6da2368c --- /dev/null +++ b/components/x-topic-search/src/NoSuggestions.jsx @@ -0,0 +1,22 @@ +import { h } from '@financial-times/x-engine'; +import styles from './TopicSearch.scss'; +import classNames from 'classnames'; + +export default ({ searchTerm }) => ( +
  • + +

    + No topics matching { searchTerm } +

    + +

    Suggestions:

    + +
      +
    • Make sure that all words are spelled correctly.
    • +
    • Try different keywords.
    • +
    • Try more general keywords.
    • +
    • Try fewer keywords.
    • +
    + +
  • +); diff --git a/components/x-topic-search/src/ResultContainer.jsx b/components/x-topic-search/src/ResultContainer.jsx new file mode 100644 index 000000000..2ede0ebcd --- /dev/null +++ b/components/x-topic-search/src/ResultContainer.jsx @@ -0,0 +1,46 @@ +import { h } from '@financial-times/x-engine'; +import styles from './TopicSearch.scss'; +import classNames from 'classnames'; + +import SuggestionList from './SuggestionList'; +import NoSuggestions from './NoSuggestions'; + +const resultContainerClassNames = [ + 'n-ui-hide-core', + styles['result-container'] +].join(' '); + +// transform like this => topic1, topic2 and topic3 +const transformFollowingTopics = currentlyFollowingTopics => { + const topicsLength = currentlyFollowingTopics.length; + + return currentlyFollowingTopics + .map((topic, index) => { + if (index + 1 === topicsLength) { + // the last topic + return and { topic.name } + } else { + if ((topicsLength - 2) === index) { + // one before the last topic + return { topic.name } ; + } else { + return { topic.name }, ; + } + } + }) +}; + +export default ({ result, searchTerm }) => ( +
    +
      + + { result.status === 'suggestions' && } + + { result.status === 'no-suggestions' && } + + { result.status === 'all-followed' && +
    • You already follow { transformFollowingTopics(result.followingTopicsIncludeSearchTerm) }
    • } + +
    +
    +); diff --git a/components/x-topic-search/src/SuggestionList.jsx b/components/x-topic-search/src/SuggestionList.jsx new file mode 100644 index 000000000..03e931c8b --- /dev/null +++ b/components/x-topic-search/src/SuggestionList.jsx @@ -0,0 +1,22 @@ +import { h } from '@financial-times/x-engine'; +import styles from './TopicSearch.scss'; +import classNames from 'classnames'; + +export default ({ suggestions, searchTerm }) => { + // TODO use x-follow-button + const listResults = suggestions.map((suggestion, index) => ( +
  • + + { suggestion.prefLabel } + + + +
  • + )) + + return listResults; +}; diff --git a/components/x-topic-search/src/TopicSearch.jsx b/components/x-topic-search/src/TopicSearch.jsx index 932a3f4fc..eb8bcc840 100644 --- a/components/x-topic-search/src/TopicSearch.jsx +++ b/components/x-topic-search/src/TopicSearch.jsx @@ -1,10 +1,77 @@ import { h } from '@financial-times/x-engine'; +import { withActions } from '@financial-times/x-interaction'; +import styles from './TopicSearch.scss'; +import classNames from 'classnames'; +import getSuggestions from './lib/get-suggestions.js'; +import debounce from 'debounce-promise'; +import ResultContainer from './ResultContainer'; + +const containerClassNames = [ + 'n-ui-hide-core', + styles['container'] +].join(' '); + +const debounceGetSuggestions = debounce(getSuggestions, 150); + +let resultExist = false; + +const topicSearchActions = withActions(({ minSearchLength = 2, maxSuggestions = 5, apiUrl, currentlyFollowingTopics }) => ({ + async checkInput(event) { + const searchTerm = event.target.value && event.target.value.trim(); + + if (searchTerm.length >= minSearchLength) { + return debounceGetSuggestions(searchTerm, maxSuggestions, apiUrl, currentlyFollowingTopics) + .then(result => { + resultExist = true; + return { showResult: true, result, searchTerm }; + }) + .catch(() => { + resultExist = false; + return { showResult: false }; + }); + } else { + resultExist = false; + return Promise.resolve({ showResult: false }); + } + }, + + selectInput (event) { + event.target.select(); + return { showResult: resultExist }; + }, + + hideResult() { + return { showResult: false }; + } +})); + +const TopicSearch = topicSearchActions((props) => ( +
    +

    + Search for topics, authors, companies, or other areas of interest +

    + + +
    + + +
    + + { props.showResult && !props.isLoading && } -const TopicSearch = (props) => ( -
    -

    Welcome to x-topic-search

    -

    {props.message}

    -); +)); export { TopicSearch }; diff --git a/components/x-topic-search/src/TopicSearch.scss b/components/x-topic-search/src/TopicSearch.scss new file mode 100644 index 000000000..73bd26d34 --- /dev/null +++ b/components/x-topic-search/src/TopicSearch.scss @@ -0,0 +1,106 @@ +@import 'o-icons/main'; +@import 'o-colors/main'; +@import 'o-typography/main'; +@import 'o-forms/main'; + +.container { + @include oFormsBaseFeatures; + @include oFormsWideFeature; + position: relative; + text-align: center; + background-color: oColorsGetPaletteColor('claret-70'); + color: oColorsGetPaletteColor('white'); +} + +.input-wrapper { + position: relative; + padding: 10px; +} + +.search-icon { + @include oIconsGetIcon('search', oColorsGetPaletteColor('white'), 32); + position: absolute; + top: 14px; + left: 3px; +} + +.input { + @include oFormsCommonFieldBase; + @include oTypographySans($scale: 0); + margin: 0; + border: none; + border-bottom: 2px solid oColorsGetPaletteColor('white'); + padding-left: 24px; + max-width: none; + color: oColorsGetPaletteColor('white'); + background: transparent; + + &::placeholder { + color: oColorsGetPaletteColor('white'); + } +} + +.result-container { + position: absolute; + background: oColorsGetPaletteColor('white'); + top: 60px; + padding: 10px; + z-index: 1; + width: calc(100% - 20px); +} + +.suggestions { + list-style: none; + padding: 0; + text-align: left; + margin: 0; +} + +.suggestion { + display: flex; + justify-content: space-between; + align-items: center; + clear: right; + padding: 5px 0; + border-bottom: 1px solid oColorsGetPaletteColor('black-5'); + + &:last-child { + border-bottom: 0; + padding-bottom: 0; + } + + &:first-child { + padding-top: 0; + } + +} + +.suggestion__name { + @include oTypographySansBold($scale: -2); + @include oTypographyTopic(); + padding: 5px 0; + max-width: 50%; + word-wrap: break-word; + overflow-wrap: break-word; +} + +.no-suggestions { + @include oTypographySans( $scale: 1); + color: oColorsGetPaletteColor('black-70'); + :nth-child(2) { + margin: 0; + } +} + +.no-suggestions__title { + @include oTypographySans( $scale: 3); + font-weight: normal; + margin: 0; + padding: 0 0 20px; +} + +.no-suggestions__message { + margin-top: 12px; + margin-bottom: 0; + list-style-type: disc; +} diff --git a/components/x-topic-search/src/lib/get-suggestions.js b/components/x-topic-search/src/lib/get-suggestions.js new file mode 100644 index 000000000..7fed48b81 --- /dev/null +++ b/components/x-topic-search/src/lib/get-suggestions.js @@ -0,0 +1,52 @@ +const addQueryParamToUrl = (name, value, url, append = true) => { + const queryParam = `${name}=${value}`; + return append === true ? `${url}&${queryParam}` : `${url}?${queryParam}`; +}; + +const suggest = function (suggestions, currentlyFollowingTopics, searchTerm) { + if (suggestions.length) { + suggestions.forEach((suggestion) => { + if (suggestion && !suggestion.url){ + // TODO App needs different url? + suggestion.url = '/stream/' + suggestion.id; + }; + }); + return { status: 'suggestions', suggestions }; + } else { + const followingTopicsIncludeSearchTerm = currentlyFollowingTopics + .filter(topic => topic.name.toLowerCase().includes(searchTerm.toLowerCase())); + + if(followingTopicsIncludeSearchTerm.length > 0) { + return { status: 'all-followed', followingTopicsIncludeSearchTerm }; + } + + return { status: 'no-suggestions' }; + } +}; + + +export default (searchTerm, maxSuggestions, apiUrl, currentlyFollowingTopics) => { + + const dataSrc = addQueryParamToUrl('count', maxSuggestions, apiUrl, false); + const tagged = currentlyFollowingTopics + .map(topic => topic.uuid) + .join(','); + + let url = addQueryParamToUrl('partial', searchTerm.replace(' ', '+'), dataSrc); + url = addQueryParamToUrl('tagged', tagged, url); + + return fetch(url) + .then(response => { + if (!response.ok) { + throw new Error(response.statusText); + } + return response.json(); + }) + .then(suggestions => { + return suggest(suggestions, currentlyFollowingTopics, searchTerm) + }) + .catch(() => { + throw new Error(); + }); + +}; diff --git a/components/x-topic-search/stories/example.js b/components/x-topic-search/stories/example.js deleted file mode 100644 index 6df941bf7..000000000 --- a/components/x-topic-search/stories/example.js +++ /dev/null @@ -1,9 +0,0 @@ -exports.title = 'Example'; - -exports.data = { - message: 'Hello World!' -}; - -// This reference is only required for hot module loading in development -// -exports.m = module; diff --git a/components/x-topic-search/stories/index.js b/components/x-topic-search/stories/index.js index 26239e05c..419ff844a 100644 --- a/components/x-topic-search/stories/index.js +++ b/components/x-topic-search/stories/index.js @@ -7,9 +7,11 @@ exports.package = require('../package.json'); // Set up basic document styling using the Origami build service exports.dependencies = { 'o-normalise': '^1.6.0', - 'o-typography': '^5.5.0' + 'o-typography': '^5.5.0', + 'o-colors': "^4.7.7", + 'o-icons': "^5.8.0", }; exports.stories = [ - require('./example') + require('./topic-search') ]; diff --git a/components/x-topic-search/stories/topic-search.js b/components/x-topic-search/stories/topic-search.js new file mode 100644 index 000000000..23a938051 --- /dev/null +++ b/components/x-topic-search/stories/topic-search.js @@ -0,0 +1,17 @@ +exports.title = 'Topic Search Bar'; + +exports.data = { + minSearchLength: 2, + maxSuggestions: 10, + apiUrl: '//tag-facets-api.ft.com/annotations', + currentlyFollowingTopics: [ + { + name: 'World Elephant Polo Association', + uuid: 'f95d1e16-2307-4feb-b3ff-6f224798aa49' + } + ] +}; + +// This reference is only required for hot module loading in development +// +exports.m = module; From 3678a13bbade31cf52d64bd080d7583f6b12bd6a Mon Sep 17 00:00:00 2001 From: fenglish Date: Fri, 16 Nov 2018 10:45:40 +0000 Subject: [PATCH 03/71] change currentlyFollowingTopics = => followedTopics --- components/x-topic-search/readme.md | 12 ++++++------ components/x-topic-search/src/ResultContainer.jsx | 8 ++++---- components/x-topic-search/src/TopicSearch.jsx | 4 ++-- .../x-topic-search/src/lib/get-suggestions.js | 14 +++++++------- components/x-topic-search/stories/topic-search.js | 2 +- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/components/x-topic-search/readme.md b/components/x-topic-search/readme.md index cf4c9c989..5e86724b0 100644 --- a/components/x-topic-search/readme.md +++ b/components/x-topic-search/readme.md @@ -36,9 +36,9 @@ All `x-` components are designed to be compatible with a variety of runtimes, no ### Properties -Property | Type | Required | Note -------------------------|--------|----------|------------------ -`minSearchLength` | Number | No | Minimum chars to start search. Default is 2 -`maxSuggestions` | Number | No | Maximum number to display suggestions. Default is 5 -`apiUrl` | String | Yes | The url to use when making requests to get topics -`currentlyFollowingTopics` | Array | Yes | Each item should have `name` and `uuid` properties +Property | Type | Required | Note +-----------------|--------|----------|------------------ +`minSearchLength`| Number | No | Minimum chars to start search. Default is 2 +`maxSuggestions` | Number | No | Maximum number to display suggestions. Default is 5 +`apiUrl` | String | Yes | The url to use when making requests to get topics +`followedTopics` | Array | Yes | Each item should have `name` and `uuid` properties diff --git a/components/x-topic-search/src/ResultContainer.jsx b/components/x-topic-search/src/ResultContainer.jsx index 2ede0ebcd..77c3b15de 100644 --- a/components/x-topic-search/src/ResultContainer.jsx +++ b/components/x-topic-search/src/ResultContainer.jsx @@ -11,10 +11,10 @@ const resultContainerClassNames = [ ].join(' '); // transform like this => topic1, topic2 and topic3 -const transformFollowingTopics = currentlyFollowingTopics => { - const topicsLength = currentlyFollowingTopics.length; +const transformFollowedTopics = followedTopicsIncludeSearchTerm => { + const topicsLength = followedTopicsIncludeSearchTerm.length; - return currentlyFollowingTopics + return followedTopicsIncludeSearchTerm .map((topic, index) => { if (index + 1 === topicsLength) { // the last topic @@ -39,7 +39,7 @@ export default ({ result, searchTerm }) => ( { result.status === 'no-suggestions' && } { result.status === 'all-followed' && -
  • You already follow { transformFollowingTopics(result.followingTopicsIncludeSearchTerm) }
  • } +
  • You already follow { transformFollowedTopics(result.followedTopicsIncludeSearchTerm) }
  • }
    diff --git a/components/x-topic-search/src/TopicSearch.jsx b/components/x-topic-search/src/TopicSearch.jsx index eb8bcc840..a337b0ebf 100644 --- a/components/x-topic-search/src/TopicSearch.jsx +++ b/components/x-topic-search/src/TopicSearch.jsx @@ -15,12 +15,12 @@ const debounceGetSuggestions = debounce(getSuggestions, 150); let resultExist = false; -const topicSearchActions = withActions(({ minSearchLength = 2, maxSuggestions = 5, apiUrl, currentlyFollowingTopics }) => ({ +const topicSearchActions = withActions(({ minSearchLength = 2, maxSuggestions = 5, apiUrl, followedTopics }) => ({ async checkInput(event) { const searchTerm = event.target.value && event.target.value.trim(); if (searchTerm.length >= minSearchLength) { - return debounceGetSuggestions(searchTerm, maxSuggestions, apiUrl, currentlyFollowingTopics) + return debounceGetSuggestions(searchTerm, maxSuggestions, apiUrl, followedTopics) .then(result => { resultExist = true; return { showResult: true, result, searchTerm }; diff --git a/components/x-topic-search/src/lib/get-suggestions.js b/components/x-topic-search/src/lib/get-suggestions.js index 7fed48b81..9db60a3c3 100644 --- a/components/x-topic-search/src/lib/get-suggestions.js +++ b/components/x-topic-search/src/lib/get-suggestions.js @@ -3,7 +3,7 @@ const addQueryParamToUrl = (name, value, url, append = true) => { return append === true ? `${url}&${queryParam}` : `${url}?${queryParam}`; }; -const suggest = function (suggestions, currentlyFollowingTopics, searchTerm) { +const suggest = function (suggestions, followedTopics, searchTerm) { if (suggestions.length) { suggestions.forEach((suggestion) => { if (suggestion && !suggestion.url){ @@ -13,11 +13,11 @@ const suggest = function (suggestions, currentlyFollowingTopics, searchTerm) { }); return { status: 'suggestions', suggestions }; } else { - const followingTopicsIncludeSearchTerm = currentlyFollowingTopics + const followedTopicsIncludeSearchTerm = followedTopics .filter(topic => topic.name.toLowerCase().includes(searchTerm.toLowerCase())); - if(followingTopicsIncludeSearchTerm.length > 0) { - return { status: 'all-followed', followingTopicsIncludeSearchTerm }; + if(followedTopicsIncludeSearchTerm.length > 0) { + return { status: 'all-followed', followedTopicsIncludeSearchTerm }; } return { status: 'no-suggestions' }; @@ -25,10 +25,10 @@ const suggest = function (suggestions, currentlyFollowingTopics, searchTerm) { }; -export default (searchTerm, maxSuggestions, apiUrl, currentlyFollowingTopics) => { +export default (searchTerm, maxSuggestions, apiUrl, followedTopics) => { const dataSrc = addQueryParamToUrl('count', maxSuggestions, apiUrl, false); - const tagged = currentlyFollowingTopics + const tagged = followedTopics .map(topic => topic.uuid) .join(','); @@ -43,7 +43,7 @@ export default (searchTerm, maxSuggestions, apiUrl, currentlyFollowingTopics) => return response.json(); }) .then(suggestions => { - return suggest(suggestions, currentlyFollowingTopics, searchTerm) + return suggest(suggestions, followedTopics, searchTerm) }) .catch(() => { throw new Error(); diff --git a/components/x-topic-search/stories/topic-search.js b/components/x-topic-search/stories/topic-search.js index 23a938051..3c794f5fb 100644 --- a/components/x-topic-search/stories/topic-search.js +++ b/components/x-topic-search/stories/topic-search.js @@ -4,7 +4,7 @@ exports.data = { minSearchLength: 2, maxSuggestions: 10, apiUrl: '//tag-facets-api.ft.com/annotations', - currentlyFollowingTopics: [ + followedTopics: [ { name: 'World Elephant Polo Association', uuid: 'f95d1e16-2307-4feb-b3ff-6f224798aa49' From 85ba40778e7ff0127743d2bc412b151d02f7a6d9 Mon Sep 17 00:00:00 2001 From: fenglish Date: Fri, 16 Nov 2018 11:07:56 +0000 Subject: [PATCH 04/71] make JSX being its own JSX component --- components/x-topic-search/src/AllFollowed.jsx | 29 ++++++++++++ .../x-topic-search/src/NoSuggestions.jsx | 24 +++++----- .../x-topic-search/src/ResultContainer.jsx | 46 ------------------- .../x-topic-search/src/SuggestionList.jsx | 25 ++++++---- components/x-topic-search/src/TopicSearch.jsx | 34 ++++++++++---- .../x-topic-search/src/TopicSearch.scss | 18 ++++++-- 6 files changed, 98 insertions(+), 78 deletions(-) create mode 100644 components/x-topic-search/src/AllFollowed.jsx delete mode 100644 components/x-topic-search/src/ResultContainer.jsx diff --git a/components/x-topic-search/src/AllFollowed.jsx b/components/x-topic-search/src/AllFollowed.jsx new file mode 100644 index 000000000..d43683849 --- /dev/null +++ b/components/x-topic-search/src/AllFollowed.jsx @@ -0,0 +1,29 @@ +import { h } from '@financial-times/x-engine'; +import styles from './TopicSearch.scss'; +import classNames from 'classnames'; + +// transform like this => topic1, topic2 and topic3 +const transformFollowedTopics = followedTopicsIncludeSearchTerm => { + const topicsLength = followedTopicsIncludeSearchTerm.length; + + return followedTopicsIncludeSearchTerm + .map((topic, index) => { + if (index + 1 === topicsLength) { + // the last topic + return and { topic.name } + } else { + if ((topicsLength - 2) === index) { + // one before the last topic + return { topic.name } ; + } else { + return { topic.name }, ; + } + } + }) +}; + +export default ({ followedTopicsIncludeSearchTerm }) => ( +
    + You already follow { transformFollowedTopics(followedTopicsIncludeSearchTerm) } +
    +); diff --git a/components/x-topic-search/src/NoSuggestions.jsx b/components/x-topic-search/src/NoSuggestions.jsx index c6da2368c..444c1adb6 100644 --- a/components/x-topic-search/src/NoSuggestions.jsx +++ b/components/x-topic-search/src/NoSuggestions.jsx @@ -3,20 +3,20 @@ import styles from './TopicSearch.scss'; import classNames from 'classnames'; export default ({ searchTerm }) => ( -
  • +
    -

    - No topics matching { searchTerm } -

    +

    + No topics matching { searchTerm } +

    -

    Suggestions:

    +

    Suggestions:

    -
      -
    • Make sure that all words are spelled correctly.
    • -
    • Try different keywords.
    • -
    • Try more general keywords.
    • -
    • Try fewer keywords.
    • -
    +
      +
    • Make sure that all words are spelled correctly.
    • +
    • Try different keywords.
    • +
    • Try more general keywords.
    • +
    • Try fewer keywords.
    • +
    -
  • + ); diff --git a/components/x-topic-search/src/ResultContainer.jsx b/components/x-topic-search/src/ResultContainer.jsx deleted file mode 100644 index 77c3b15de..000000000 --- a/components/x-topic-search/src/ResultContainer.jsx +++ /dev/null @@ -1,46 +0,0 @@ -import { h } from '@financial-times/x-engine'; -import styles from './TopicSearch.scss'; -import classNames from 'classnames'; - -import SuggestionList from './SuggestionList'; -import NoSuggestions from './NoSuggestions'; - -const resultContainerClassNames = [ - 'n-ui-hide-core', - styles['result-container'] -].join(' '); - -// transform like this => topic1, topic2 and topic3 -const transformFollowedTopics = followedTopicsIncludeSearchTerm => { - const topicsLength = followedTopicsIncludeSearchTerm.length; - - return followedTopicsIncludeSearchTerm - .map((topic, index) => { - if (index + 1 === topicsLength) { - // the last topic - return and { topic.name } - } else { - if ((topicsLength - 2) === index) { - // one before the last topic - return { topic.name } ; - } else { - return { topic.name }, ; - } - } - }) -}; - -export default ({ result, searchTerm }) => ( -
    -
      - - { result.status === 'suggestions' && } - - { result.status === 'no-suggestions' && } - - { result.status === 'all-followed' && -
    • You already follow { transformFollowedTopics(result.followedTopicsIncludeSearchTerm) }
    • } - -
    -
    -); diff --git a/components/x-topic-search/src/SuggestionList.jsx b/components/x-topic-search/src/SuggestionList.jsx index 03e931c8b..6bb6d90a7 100644 --- a/components/x-topic-search/src/SuggestionList.jsx +++ b/components/x-topic-search/src/SuggestionList.jsx @@ -5,18 +5,25 @@ import classNames from 'classnames'; export default ({ suggestions, searchTerm }) => { // TODO use x-follow-button const listResults = suggestions.map((suggestion, index) => ( -
  • - { suggestion.prefLabel } +
  • - + + { suggestion.prefLabel } + + + + +
  • - )) - return listResults; + return ( +
      + { listResults } +
    ); }; diff --git a/components/x-topic-search/src/TopicSearch.jsx b/components/x-topic-search/src/TopicSearch.jsx index a337b0ebf..52b66b524 100644 --- a/components/x-topic-search/src/TopicSearch.jsx +++ b/components/x-topic-search/src/TopicSearch.jsx @@ -4,13 +4,22 @@ import styles from './TopicSearch.scss'; import classNames from 'classnames'; import getSuggestions from './lib/get-suggestions.js'; import debounce from 'debounce-promise'; -import ResultContainer from './ResultContainer'; + +import SuggestionList from './SuggestionList'; +import NoSuggestions from './NoSuggestions'; +import AllFollowed from './AllFollowed'; const containerClassNames = [ 'n-ui-hide-core', styles['container'] ].join(' '); +const resultContainerClassNames = [ + 'n-ui-hide-core', + styles['result-container'] +].join(' '); + + const debounceGetSuggestions = debounce(getSuggestions, 150); let resultExist = false; @@ -45,7 +54,7 @@ const topicSearchActions = withActions(({ minSearchLength = 2, maxSuggestions = } })); -const TopicSearch = topicSearchActions((props) => ( +const TopicSearch = topicSearchActions(({ searchTerm, showResult, result, actions, isLoading }) => (

    Search for topics, authors, companies, or other areas of interest @@ -62,14 +71,23 @@ const TopicSearch = topicSearchActions((props) => ( placeholder="Search and add topics" className={ classNames(styles["input"]) } data-trackable="topic-search" - onChange={ props.actions.checkInput } - onClick={ props.actions.selectInput } - onFocus={ props.actions.selectInput } - onBlur={ props.actions.hideResult } - /> + onChange={ actions.checkInput } + onClick={ actions.selectInput } + onFocus={ actions.selectInput } + onBlur={ actions.hideResult }/>

    - { props.showResult && !props.isLoading && } + { showResult && !isLoading && +
    + { result.status === 'suggestions'&& + } + + { result.status === 'no-suggestions' && + } + + { result.status === 'all-followed' && + } +
    } )); diff --git a/components/x-topic-search/src/TopicSearch.scss b/components/x-topic-search/src/TopicSearch.scss index 73bd26d34..6d7944041 100644 --- a/components/x-topic-search/src/TopicSearch.scss +++ b/components/x-topic-search/src/TopicSearch.scss @@ -85,16 +85,20 @@ } .no-suggestions { - @include oTypographySans( $scale: 1); + @include oTypographySans($scale: 1); color: oColorsGetPaletteColor('black-70'); - :nth-child(2) { + text-align: left; + padding: 0 0 5px 0; + margin: 0; + p { margin: 0; } } .no-suggestions__title { - @include oTypographySans( $scale: 3); + @include oTypographySans($scale: 3); font-weight: normal; + overflow-wrap: break-word; margin: 0; padding: 0 0 20px; } @@ -104,3 +108,11 @@ margin-bottom: 0; list-style-type: disc; } + +.all-followed { + @include oTypographySans($scale: 1); + color: oColorsGetPaletteColor('black-70'); + text-align: left; + padding: 0; + margin: 0; +} From ddbdb0656b096065fa142b05ba80f267930b7480 Mon Sep 17 00:00:00 2001 From: fenglish Date: Fri, 16 Nov 2018 15:24:23 +0000 Subject: [PATCH 05/71] reflect reviews --- components/x-topic-search/package.json | 2 +- components/x-topic-search/src/AllFollowed.jsx | 10 +++++----- components/x-topic-search/src/TopicSearch.jsx | 4 ++-- components/x-topic-search/src/lib/get-suggestions.js | 6 +++--- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/components/x-topic-search/package.json b/components/x-topic-search/package.json index 19defd952..639734f75 100644 --- a/components/x-topic-search/package.json +++ b/components/x-topic-search/package.json @@ -7,7 +7,7 @@ "browser": "dist/TopicSearch.es5.js", "style": "dist/TopicSearch.css", "scripts": { - "prepare": "npm run build", + "prepare": "bower install && npm run build", "build": "node rollup.js", "start": "node rollup.js --watch" }, diff --git a/components/x-topic-search/src/AllFollowed.jsx b/components/x-topic-search/src/AllFollowed.jsx index d43683849..8d14547c7 100644 --- a/components/x-topic-search/src/AllFollowed.jsx +++ b/components/x-topic-search/src/AllFollowed.jsx @@ -3,10 +3,10 @@ import styles from './TopicSearch.scss'; import classNames from 'classnames'; // transform like this => topic1, topic2 and topic3 -const transformFollowedTopics = followedTopicsIncludeSearchTerm => { - const topicsLength = followedTopicsIncludeSearchTerm.length; +const arrayToSentence = matchingFollowedTopics => { + const topicsLength = matchingFollowedTopics.length; - return followedTopicsIncludeSearchTerm + return matchingFollowedTopics .map((topic, index) => { if (index + 1 === topicsLength) { // the last topic @@ -22,8 +22,8 @@ const transformFollowedTopics = followedTopicsIncludeSearchTerm => { }) }; -export default ({ followedTopicsIncludeSearchTerm }) => ( +export default ({ matchingFollowedTopics }) => (
    - You already follow { transformFollowedTopics(followedTopicsIncludeSearchTerm) } + You already follow { arrayToSentence(matchingFollowedTopics) }
    ); diff --git a/components/x-topic-search/src/TopicSearch.jsx b/components/x-topic-search/src/TopicSearch.jsx index 52b66b524..5b45a0e14 100644 --- a/components/x-topic-search/src/TopicSearch.jsx +++ b/components/x-topic-search/src/TopicSearch.jsx @@ -64,7 +64,7 @@ const TopicSearch = topicSearchActions(({ searchTerm, showResult, result, action Search and add topics
    - + } { result.status === 'all-followed' && - } + }
    } diff --git a/components/x-topic-search/src/lib/get-suggestions.js b/components/x-topic-search/src/lib/get-suggestions.js index 9db60a3c3..72c83dbba 100644 --- a/components/x-topic-search/src/lib/get-suggestions.js +++ b/components/x-topic-search/src/lib/get-suggestions.js @@ -13,11 +13,11 @@ const suggest = function (suggestions, followedTopics, searchTerm) { }); return { status: 'suggestions', suggestions }; } else { - const followedTopicsIncludeSearchTerm = followedTopics + const matchingFollowedTopics = followedTopics .filter(topic => topic.name.toLowerCase().includes(searchTerm.toLowerCase())); - if(followedTopicsIncludeSearchTerm.length > 0) { - return { status: 'all-followed', followedTopicsIncludeSearchTerm }; + if(matchingFollowedTopics.length > 0) { + return { status: 'all-followed', matchingFollowedTopics }; } return { status: 'no-suggestions' }; From 69753c05d0ffae7981e6d67c47aea41095b67cf9 Mon Sep 17 00:00:00 2001 From: fenglish Date: Fri, 16 Nov 2018 16:42:08 +0000 Subject: [PATCH 06/71] use x-follow-button --- components/x-topic-search/package.json | 1 + components/x-topic-search/src/SuggestionList.jsx | 7 ++++--- components/x-topic-search/src/TopicSearch.scss | 4 ++++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/components/x-topic-search/package.json b/components/x-topic-search/package.json index 639734f75..e620319fa 100644 --- a/components/x-topic-search/package.json +++ b/components/x-topic-search/package.json @@ -18,6 +18,7 @@ "license": "ISC", "dependencies": { "@financial-times/x-engine": "file:../../packages/x-engine", + "@financial-times/x-follow-button": "0.0.2", "@financial-times/x-interaction": "^1.0.0-beta.6", "classnames": "^2.2.6", "debounce-promise": "^3.1.0" diff --git a/components/x-topic-search/src/SuggestionList.jsx b/components/x-topic-search/src/SuggestionList.jsx index 6bb6d90a7..8c3f0d0e9 100644 --- a/components/x-topic-search/src/SuggestionList.jsx +++ b/components/x-topic-search/src/SuggestionList.jsx @@ -1,22 +1,23 @@ import { h } from '@financial-times/x-engine'; +import { FollowButton } from '@financial-times/x-follow-button'; import styles from './TopicSearch.scss'; import classNames from 'classnames'; export default ({ suggestions, searchTerm }) => { - // TODO use x-follow-button + const listResults = suggestions.map((suggestion, index) => (
  • { suggestion.prefLabel } - +
  • diff --git a/components/x-topic-search/src/TopicSearch.scss b/components/x-topic-search/src/TopicSearch.scss index 6d7944041..6867d21d0 100644 --- a/components/x-topic-search/src/TopicSearch.scss +++ b/components/x-topic-search/src/TopicSearch.scss @@ -3,6 +3,10 @@ @import 'o-typography/main'; @import 'o-forms/main'; +:global { + @import "~@financial-times/x-follow-button/dist/FollowButton"; +} + .container { @include oFormsBaseFeatures; @include oFormsWideFeature; From c5702acc17dc1be1808ada9a1c9c93345f0141fc Mon Sep 17 00:00:00 2001 From: fenglish Date: Mon, 19 Nov 2018 16:02:25 +0000 Subject: [PATCH 07/71] add snapshot tests --- .../x-topic-search.test.jsx.snap | 210 ++++++++++++++++++ .../__tests__/x-topic-search.test.jsx | 69 ++++++ components/x-topic-search/package.json | 5 +- components/x-topic-search/src/TopicSearch.jsx | 2 +- .../x-topic-search/src/lib/get-suggestions.js | 12 +- package.json | 3 +- 6 files changed, 292 insertions(+), 9 deletions(-) create mode 100644 components/x-topic-search/__tests__/__snapshots__/x-topic-search.test.jsx.snap create mode 100644 components/x-topic-search/__tests__/x-topic-search.test.jsx diff --git a/components/x-topic-search/__tests__/__snapshots__/x-topic-search.test.jsx.snap b/components/x-topic-search/__tests__/__snapshots__/x-topic-search.test.jsx.snap new file mode 100644 index 000000000..f70874757 --- /dev/null +++ b/components/x-topic-search/__tests__/__snapshots__/x-topic-search.test.jsx.snap @@ -0,0 +1,210 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`x-topic-search given all topics include search term are followed should render followed topics name list 1`] = ` +
    +

    + Search for topics, authors, companies, or other areas of interest +

    + +
    + + +
    +
    +
    + You already follow + + + abcc + + + + + and + + abccd + + +
    +
    +
    +`; + +exports[`x-topic-search given there are unfollowed topics include search term should render the unfollowed topics list with x-follow-button 1`] = ` +
    +

    + Search for topics, authors, companies, or other areas of interest +

    + +
    + + +
    +
    + +
    +
    +`; + +exports[`x-topic-search given there is no topics include search term exist should render no topics message 1`] = ` +
    +

    + Search for topics, authors, companies, or other areas of interest +

    + +
    + + +
    +
    +`; diff --git a/components/x-topic-search/__tests__/x-topic-search.test.jsx b/components/x-topic-search/__tests__/x-topic-search.test.jsx new file mode 100644 index 000000000..04c453ec2 --- /dev/null +++ b/components/x-topic-search/__tests__/x-topic-search.test.jsx @@ -0,0 +1,69 @@ +const { h } = require('@financial-times/x-engine'); +const { mount } = require('@financial-times/x-test-utils/enzyme'); +const { TopicSearch } = require('../'); +const fetchMock = require('fetch-mock'); + +const maxSuggestions = 3; +const searchTerm = 'abc'; +const apiUrl = 'api-url'; +const apiUrlWithQueries = `${ apiUrl }?count=${ maxSuggestions }&partial=${ searchTerm }`; +const props = { apiUrl, maxSuggestions }; + +describe('x-topic-search', () => { + + let fetchUrl; + + beforeEach(() => { + fetchUrl = apiUrlWithQueries; + }); + + afterEach(() => { + fetchMock.restore(); + }); + + describe('given there are unfollowed topics include search term', () => { + it('should render the unfollowed topics list with x-follow-button', async () => { + const apiResponse = [ + { id: 'TOPIC-1__id', prefLabel: 'TOPIC-1__name', url:'TOPIC-1__url' }, + { id: 'TOPIC-2__id', prefLabel: 'TOPIC-2__name', url:'TOPIC-2__url' } + ]; + fetchMock.get(fetchUrl, apiResponse); + + const subject = mount(); + const input = subject.find('input'); + await input.prop('onChange')({ target: { value: searchTerm }}); + + expect(subject.render()).toMatchSnapshot(); + }) + }); + + describe('given there is no topics include search term exist', () => { + it('should render no topics message', async () => { + fetchMock.get(fetchUrl, []); + + const subject = mount(); + const input = subject.find('input'); + await input.prop('onChange')({ target: { value: searchTerm }}); + + expect(subject.render()).toMatchSnapshot(); + }) + }); + + describe('given all topics include search term are followed', () => { + it('should render followed topics name list', async () => { + const followedTopicOne = { name: `${searchTerm}c`, uuid: 'FOLLOWED-TOPIC-1__id' }; + const followedTopicTwo = { name: `${searchTerm}cd`, uuid: 'FOLLOWED-TOPIC-2__id' }; + props.followedTopics = [ followedTopicOne, followedTopicTwo ]; + + fetchUrl = `${ apiUrlWithQueries }&tagged=${ followedTopicOne.uuid },${ followedTopicTwo.uuid }`; + fetchMock.get(fetchUrl, []); + + const subject = mount(); + const input = subject.find('input'); + await input.prop('onChange')({ target: { value: searchTerm }}); + + expect(subject.render()).toMatchSnapshot(); + }) + }); + +}); diff --git a/components/x-topic-search/package.json b/components/x-topic-search/package.json index e620319fa..f62b11802 100644 --- a/components/x-topic-search/package.json +++ b/components/x-topic-search/package.json @@ -25,8 +25,9 @@ }, "devDependencies": { "@financial-times/x-rollup": "file:../../packages/x-rollup", - "node-sass": "^4.9.2", - "bower": "^1.7.9" + "@financial-times/x-test-utils": "file:../../packages/x-test-utils", + "bower": "^1.7.9", + "node-sass": "^4.9.2" }, "repository": { "type": "git", diff --git a/components/x-topic-search/src/TopicSearch.jsx b/components/x-topic-search/src/TopicSearch.jsx index 5b45a0e14..b8556281c 100644 --- a/components/x-topic-search/src/TopicSearch.jsx +++ b/components/x-topic-search/src/TopicSearch.jsx @@ -24,7 +24,7 @@ const debounceGetSuggestions = debounce(getSuggestions, 150); let resultExist = false; -const topicSearchActions = withActions(({ minSearchLength = 2, maxSuggestions = 5, apiUrl, followedTopics }) => ({ +const topicSearchActions = withActions(({ minSearchLength = 2, maxSuggestions = 5, apiUrl, followedTopics = [] }) => ({ async checkInput(event) { const searchTerm = event.target.value && event.target.value.trim(); diff --git a/components/x-topic-search/src/lib/get-suggestions.js b/components/x-topic-search/src/lib/get-suggestions.js index 72c83dbba..47d79425a 100644 --- a/components/x-topic-search/src/lib/get-suggestions.js +++ b/components/x-topic-search/src/lib/get-suggestions.js @@ -9,7 +9,7 @@ const suggest = function (suggestions, followedTopics, searchTerm) { if (suggestion && !suggestion.url){ // TODO App needs different url? suggestion.url = '/stream/' + suggestion.id; - }; + } }); return { status: 'suggestions', suggestions }; } else { @@ -28,12 +28,14 @@ const suggest = function (suggestions, followedTopics, searchTerm) { export default (searchTerm, maxSuggestions, apiUrl, followedTopics) => { const dataSrc = addQueryParamToUrl('count', maxSuggestions, apiUrl, false); - const tagged = followedTopics + let url = addQueryParamToUrl('partial', searchTerm.replace(' ', '+'), dataSrc); + + if (followedTopics.length > 0) { + const tagged = followedTopics .map(topic => topic.uuid) .join(','); - - let url = addQueryParamToUrl('partial', searchTerm.replace(' ', '+'), dataSrc); - url = addQueryParamToUrl('tagged', tagged, url); + url = addQueryParamToUrl('tagged', tagged, url); + } return fetch(url) .then(response => { diff --git a/package.json b/package.json index ce8d55ff6..1c86a4686 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "husky": "^4.0.0", "jest": "^24.8.0", "lint-staged": "^10.0.0", + "node-fetch": "^2.3.0", "node-sass": "^4.12.0", "prettier": "^2.0.2", "react": "^16.8.6", @@ -69,7 +70,7 @@ "engine": { "browser": "react", "server": "react" - } + } }, "husky": { "hooks": { From 05cb35359d65ce781242533bc2bacf8fce9132e9 Mon Sep 17 00:00:00 2001 From: fenglish Date: Tue, 20 Nov 2018 15:36:45 +0000 Subject: [PATCH 08/71] set empty array as followedTopic's default --- .../x-topic-search.test.jsx.snap | 37 +++++++++++++++++++ components/x-topic-search/readme.md | 2 +- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/components/x-topic-search/__tests__/__snapshots__/x-topic-search.test.jsx.snap b/components/x-topic-search/__tests__/__snapshots__/x-topic-search.test.jsx.snap index f70874757..c441c4a7b 100644 --- a/components/x-topic-search/__tests__/__snapshots__/x-topic-search.test.jsx.snap +++ b/components/x-topic-search/__tests__/__snapshots__/x-topic-search.test.jsx.snap @@ -206,5 +206,42 @@ exports[`x-topic-search given there is no topics include search term exist shoul type="search" /> +
    +
    +

    + No topics matching + + abc + +

    +

    + Suggestions: +

    +
      +
    • + Make sure that all words are spelled correctly. +
    • +
    • + Try different keywords. +
    • +
    • + Try more general keywords. +
    • +
    • + Try fewer keywords. +
    • +
    +
    +
    `; diff --git a/components/x-topic-search/readme.md b/components/x-topic-search/readme.md index 5e86724b0..0abf42955 100644 --- a/components/x-topic-search/readme.md +++ b/components/x-topic-search/readme.md @@ -41,4 +41,4 @@ Property | Type | Required | Note `minSearchLength`| Number | No | Minimum chars to start search. Default is 2 `maxSuggestions` | Number | No | Maximum number to display suggestions. Default is 5 `apiUrl` | String | Yes | The url to use when making requests to get topics -`followedTopics` | Array | Yes | Each item should have `name` and `uuid` properties +`followedTopics` | Array | Yes | Each item should have `name` and `uuid` properties. Default is `[]` From 5f074d4854781260f8bd06fe82cdbcedcdfb0e95 Mon Sep 17 00:00:00 2001 From: fenglish Date: Tue, 20 Nov 2018 15:42:38 +0000 Subject: [PATCH 09/71] display correct sentence when matchingFollowedTopic is only one --- components/x-topic-search/src/AllFollowed.jsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/components/x-topic-search/src/AllFollowed.jsx b/components/x-topic-search/src/AllFollowed.jsx index 8d14547c7..246fa77c7 100644 --- a/components/x-topic-search/src/AllFollowed.jsx +++ b/components/x-topic-search/src/AllFollowed.jsx @@ -6,7 +6,10 @@ import classNames from 'classnames'; const arrayToSentence = matchingFollowedTopics => { const topicsLength = matchingFollowedTopics.length; - return matchingFollowedTopics + if (topicsLength === 1) { + return { matchingFollowedTopics[0].name }; + } else { + return matchingFollowedTopics .map((topic, index) => { if (index + 1 === topicsLength) { // the last topic @@ -20,6 +23,8 @@ const arrayToSentence = matchingFollowedTopics => { } } }) + } + }; export default ({ matchingFollowedTopics }) => ( From 2cc7644694d2d7beae57c40fd8b0ff44c1ee6a32 Mon Sep 17 00:00:00 2001 From: fenglish Date: Wed, 21 Nov 2018 11:05:19 +0000 Subject: [PATCH 10/71] use onInput instead of onChange --- components/x-topic-search/__tests__/x-topic-search.test.jsx | 6 +++--- components/x-topic-search/src/TopicSearch.jsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/components/x-topic-search/__tests__/x-topic-search.test.jsx b/components/x-topic-search/__tests__/x-topic-search.test.jsx index 04c453ec2..c283dd6ac 100644 --- a/components/x-topic-search/__tests__/x-topic-search.test.jsx +++ b/components/x-topic-search/__tests__/x-topic-search.test.jsx @@ -31,7 +31,7 @@ describe('x-topic-search', () => { const subject = mount(); const input = subject.find('input'); - await input.prop('onChange')({ target: { value: searchTerm }}); + await input.prop('onInput')({ target: { value: searchTerm }}); expect(subject.render()).toMatchSnapshot(); }) @@ -43,7 +43,7 @@ describe('x-topic-search', () => { const subject = mount(); const input = subject.find('input'); - await input.prop('onChange')({ target: { value: searchTerm }}); + await input.prop('onInput')({ target: { value: searchTerm }}); expect(subject.render()).toMatchSnapshot(); }) @@ -60,7 +60,7 @@ describe('x-topic-search', () => { const subject = mount(); const input = subject.find('input'); - await input.prop('onChange')({ target: { value: searchTerm }}); + await input.prop('onInput')({ target: { value: searchTerm }}); expect(subject.render()).toMatchSnapshot(); }) diff --git a/components/x-topic-search/src/TopicSearch.jsx b/components/x-topic-search/src/TopicSearch.jsx index b8556281c..f78d3bd8a 100644 --- a/components/x-topic-search/src/TopicSearch.jsx +++ b/components/x-topic-search/src/TopicSearch.jsx @@ -71,7 +71,7 @@ const TopicSearch = topicSearchActions(({ searchTerm, showResult, result, action placeholder="Search and add topics" className={ classNames(styles["input"]) } data-trackable="topic-search" - onChange={ actions.checkInput } + onInput={ actions.checkInput } onClick={ actions.selectInput } onFocus={ actions.selectInput } onBlur={ actions.hideResult }/> From 47c745bcdc3be53cb1d46c59693741e0ec7bcd12 Mon Sep 17 00:00:00 2001 From: fenglish Date: Mon, 14 Jan 2019 15:09:43 +0000 Subject: [PATCH 11/71] Do not lint x-docs build output --- .eslintignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.eslintignore b/.eslintignore index 0fe84d9ab..a870eb0b0 100644 --- a/.eslintignore +++ b/.eslintignore @@ -8,4 +8,4 @@ **/public-prod/** **/blueprints/** web/static/** -/e2e/** \ No newline at end of file +/e2e/** From f941991a892ff68fa16e46319c072f17df1aa835 Mon Sep 17 00:00:00 2001 From: fenglish Date: Tue, 15 Jan 2019 12:46:12 +0000 Subject: [PATCH 12/71] modify styling --- components/x-topic-search/src/TopicSearch.scss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/components/x-topic-search/src/TopicSearch.scss b/components/x-topic-search/src/TopicSearch.scss index 6867d21d0..d9880361a 100644 --- a/components/x-topic-search/src/TopicSearch.scss +++ b/components/x-topic-search/src/TopicSearch.scss @@ -14,18 +14,18 @@ text-align: center; background-color: oColorsGetPaletteColor('claret-70'); color: oColorsGetPaletteColor('white'); + width: 100%; } .input-wrapper { position: relative; - padding: 10px; } .search-icon { @include oIconsGetIcon('search', oColorsGetPaletteColor('white'), 32); position: absolute; - top: 14px; - left: 3px; + top: 4px; + left: -7px; } .input { @@ -47,7 +47,7 @@ .result-container { position: absolute; background: oColorsGetPaletteColor('white'); - top: 60px; + top: 48px; padding: 10px; z-index: 1; width: calc(100% - 20px); From 2ed37031c78b4365f6c187b962149ab1f5d9d97f Mon Sep 17 00:00:00 2001 From: fenglish Date: Tue, 15 Jan 2019 14:09:02 +0000 Subject: [PATCH 13/71] delete unnecessary classes --- components/x-topic-search/src/TopicSearch.jsx | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/components/x-topic-search/src/TopicSearch.jsx b/components/x-topic-search/src/TopicSearch.jsx index f78d3bd8a..468e2917c 100644 --- a/components/x-topic-search/src/TopicSearch.jsx +++ b/components/x-topic-search/src/TopicSearch.jsx @@ -9,17 +9,6 @@ import SuggestionList from './SuggestionList'; import NoSuggestions from './NoSuggestions'; import AllFollowed from './AllFollowed'; -const containerClassNames = [ - 'n-ui-hide-core', - styles['container'] -].join(' '); - -const resultContainerClassNames = [ - 'n-ui-hide-core', - styles['result-container'] -].join(' '); - - const debounceGetSuggestions = debounce(getSuggestions, 150); let resultExist = false; @@ -55,7 +44,7 @@ const topicSearchActions = withActions(({ minSearchLength = 2, maxSuggestions = })); const TopicSearch = topicSearchActions(({ searchTerm, showResult, result, actions, isLoading }) => ( -
    +

    Search for topics, authors, companies, or other areas of interest

    @@ -78,7 +67,7 @@ const TopicSearch = topicSearchActions(({ searchTerm, showResult, result, action
    { showResult && !isLoading && -
    +
    { result.status === 'suggestions'&& } From 69ec3454c44b2266e5796f93c558271e0069227c Mon Sep 17 00:00:00 2001 From: fenglish Date: Tue, 15 Jan 2019 17:16:35 +0000 Subject: [PATCH 14/71] update x-follow-button to the latest version --- components/x-topic-search/package.json | 2 +- components/x-topic-search/src/SuggestionList.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/x-topic-search/package.json b/components/x-topic-search/package.json index f62b11802..2f272b7b7 100644 --- a/components/x-topic-search/package.json +++ b/components/x-topic-search/package.json @@ -18,7 +18,7 @@ "license": "ISC", "dependencies": { "@financial-times/x-engine": "file:../../packages/x-engine", - "@financial-times/x-follow-button": "0.0.2", + "@financial-times/x-follow-button": "0.0.6", "@financial-times/x-interaction": "^1.0.0-beta.6", "classnames": "^2.2.6", "debounce-promise": "^3.1.0" diff --git a/components/x-topic-search/src/SuggestionList.jsx b/components/x-topic-search/src/SuggestionList.jsx index 8c3f0d0e9..53160c3b0 100644 --- a/components/x-topic-search/src/SuggestionList.jsx +++ b/components/x-topic-search/src/SuggestionList.jsx @@ -17,7 +17,7 @@ export default ({ suggestions, searchTerm }) => { { suggestion.prefLabel } - + From b81fcd3d4540af57531326d7ad43c5c3c250f1f8 Mon Sep 17 00:00:00 2001 From: fenglish Date: Wed, 16 Jan 2019 14:31:38 +0000 Subject: [PATCH 15/71] update x-follow-button to v0.0.8 --- components/x-topic-search/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/x-topic-search/package.json b/components/x-topic-search/package.json index 2f272b7b7..1bf5d4f7f 100644 --- a/components/x-topic-search/package.json +++ b/components/x-topic-search/package.json @@ -18,7 +18,7 @@ "license": "ISC", "dependencies": { "@financial-times/x-engine": "file:../../packages/x-engine", - "@financial-times/x-follow-button": "0.0.6", + "@financial-times/x-follow-button": "0.0.8", "@financial-times/x-interaction": "^1.0.0-beta.6", "classnames": "^2.2.6", "debounce-promise": "^3.1.0" From 8f201df8271661349a1a3e421ce9dbea60289c36 Mon Sep 17 00:00:00 2001 From: fenglish Date: Wed, 16 Jan 2019 16:18:07 +0000 Subject: [PATCH 16/71] update x-follow-button --- components/x-topic-search/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/x-topic-search/package.json b/components/x-topic-search/package.json index 1bf5d4f7f..76e023a9e 100644 --- a/components/x-topic-search/package.json +++ b/components/x-topic-search/package.json @@ -18,7 +18,7 @@ "license": "ISC", "dependencies": { "@financial-times/x-engine": "file:../../packages/x-engine", - "@financial-times/x-follow-button": "0.0.8", + "@financial-times/x-follow-button": "0.0.9", "@financial-times/x-interaction": "^1.0.0-beta.6", "classnames": "^2.2.6", "debounce-promise": "^3.1.0" From 37c839f6fab660338ba35430d173f3b58bdb140d Mon Sep 17 00:00:00 2001 From: fenglish Date: Wed, 16 Jan 2019 16:21:59 +0000 Subject: [PATCH 17/71] update snapshots --- .../x-topic-search.test.jsx.snap | 42 ++++++------------- 1 file changed, 12 insertions(+), 30 deletions(-) diff --git a/components/x-topic-search/__tests__/__snapshots__/x-topic-search.test.jsx.snap b/components/x-topic-search/__tests__/__snapshots__/x-topic-search.test.jsx.snap index c441c4a7b..1d8adace6 100644 --- a/components/x-topic-search/__tests__/__snapshots__/x-topic-search.test.jsx.snap +++ b/components/x-topic-search/__tests__/__snapshots__/x-topic-search.test.jsx.snap @@ -2,7 +2,7 @@ exports[`x-topic-search given all topics include search term are followed should render followed topics name list 1`] = `

      -
    diff --git a/components/x-topic-search/__tests__/x-topic-search.test.jsx b/components/x-topic-search/__tests__/x-topic-search.test.jsx index 5b12a30db..a4d7c8ba5 100644 --- a/components/x-topic-search/__tests__/x-topic-search.test.jsx +++ b/components/x-topic-search/__tests__/x-topic-search.test.jsx @@ -11,7 +11,7 @@ const props = { apiUrl, maxSuggestions }; const topicOne = { id: 'TOPIC-1__id', prefLabel: `${searchTerm}d`, url:'TOPIC-1__url' }; const topicTwo = { id: 'TOPIC-2__id', prefLabel: `${searchTerm}de`, url:'TOPIC-2__url' }; -const apiResponse = [ topicOne, topicTwo]; +const topicThree = { id: 'TOPIC-3__id', prefLabel: `${searchTerm}def`, url:'TOPIC-3__url' }; describe('x-topic-search', () => { @@ -27,7 +27,7 @@ describe('x-topic-search', () => { describe('given there are unfollowed topics include search term', () => { it('should render the unfollowed topics list with x-follow-button', async () => { - fetchMock.get(fetchUrl, apiResponse); + fetchMock.get(fetchUrl, [ topicOne, topicTwo]); const subject = mount(); const input = subject.find('input'); @@ -51,9 +51,9 @@ describe('x-topic-search', () => { describe('given all topics include search term are followed', () => { it('should render followed topics name list', async () => { - props.followedTopicIds = [ topicOne.id, topicTwo.id ]; + props.followedTopicIds = [ topicOne.id, topicTwo.id, topicThree.id ]; - fetchMock.get(fetchUrl, apiResponse); + fetchMock.get(fetchUrl, [ topicOne, topicTwo, topicThree]); const subject = mount(); const input = subject.find('input'); From a6f59b4384870a3bccae6dab680e073088b3bef6 Mon Sep 17 00:00:00 2001 From: fenglish Date: Wed, 23 Jan 2019 14:56:29 +0000 Subject: [PATCH 32/71] modify followedTopicIds explanation in Readme --- components/x-topic-search/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/x-topic-search/readme.md b/components/x-topic-search/readme.md index 63a29c63a..70638432b 100644 --- a/components/x-topic-search/readme.md +++ b/components/x-topic-search/readme.md @@ -41,5 +41,5 @@ Property | Type | Required | Note `minSearchLength` | Number | No | Minimum chars to start search. Default is 2 `maxSuggestions` | Number | No | Maximum number to display suggestions. Default is 5 `apiUrl` | String | Yes | The url to use when making requests to get topics -`followedTopicIds`| Array | Yes | Array of followed topic `id`s. Default is `[]` +`followedTopicIds`| Array | Yes | Array of followed topic `id`s. `csrfToken` | String | Yes | Value included in a hidden form field for x-follow-button From 4e4d2ff0bbae814807f87ef7be36911ac349f3fc Mon Sep 17 00:00:00 2001 From: fenglish Date: Wed, 23 Jan 2019 14:59:23 +0000 Subject: [PATCH 33/71] tidy up --- components/x-topic-search/__tests__/x-topic-search.test.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/x-topic-search/__tests__/x-topic-search.test.jsx b/components/x-topic-search/__tests__/x-topic-search.test.jsx index a4d7c8ba5..c4f84a6bb 100644 --- a/components/x-topic-search/__tests__/x-topic-search.test.jsx +++ b/components/x-topic-search/__tests__/x-topic-search.test.jsx @@ -27,7 +27,7 @@ describe('x-topic-search', () => { describe('given there are unfollowed topics include search term', () => { it('should render the unfollowed topics list with x-follow-button', async () => { - fetchMock.get(fetchUrl, [ topicOne, topicTwo]); + fetchMock.get(fetchUrl, [ topicOne, topicTwo ]); const subject = mount(); const input = subject.find('input'); @@ -53,7 +53,7 @@ describe('x-topic-search', () => { it('should render followed topics name list', async () => { props.followedTopicIds = [ topicOne.id, topicTwo.id, topicThree.id ]; - fetchMock.get(fetchUrl, [ topicOne, topicTwo, topicThree]); + fetchMock.get(fetchUrl, [ topicOne, topicTwo, topicThree ]); const subject = mount(); const input = subject.find('input'); From d0df75bc7b0e7ae3dd046f1d0e8b944f66e6318d Mon Sep 17 00:00:00 2001 From: fenglish Date: Wed, 23 Jan 2019 16:20:33 +0000 Subject: [PATCH 34/71] Remove onBlur which hides result Can't click follow button in topic search result because the result disappears (this happens because of `onBlur` on ). Hiding the result container need to be controled by external triggers. --- components/x-topic-search/readme.md | 24 +++++++++++++++++++ components/x-topic-search/src/TopicSearch.jsx | 3 +-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/components/x-topic-search/readme.md b/components/x-topic-search/readme.md index 70638432b..3983fc929 100644 --- a/components/x-topic-search/readme.md +++ b/components/x-topic-search/readme.md @@ -34,6 +34,30 @@ All `x-` components are designed to be compatible with a variety of runtimes, no [jsx-wtf]: https://jasonformat.com/wtf-is-jsx/ +### Hide Result + +Your x-topic-search could hide the result container, which displays search result or messages, by external triggers. + +[x-interaction triggering-actions-externally](https://github.com/Financial-Times/x-dash/tree/master/components/x-interaction#triggering-actions-externally) + +``` +const container = ... +let topicSearchActions; + +['focusout', 'focusin', 'click'].forEach(action => { + document.body.addEventListener(action, event => { + if(!container.contains(event.target)) { + topicSearchActions.hideResult(); + } + }); +}); + +render topicSearchActions = actions} + /> +``` + ### Properties Property | Type | Required | Note diff --git a/components/x-topic-search/src/TopicSearch.jsx b/components/x-topic-search/src/TopicSearch.jsx index 5c649b4c7..1c048b42f 100644 --- a/components/x-topic-search/src/TopicSearch.jsx +++ b/components/x-topic-search/src/TopicSearch.jsx @@ -80,8 +80,7 @@ const TopicSearch = topicSearchActions(({ searchTerm, showResult, result, action data-trackable="topic-search" onInput={ actions.checkInput } onClick={ actions.selectInput } - onFocus={ actions.selectInput } - onBlur={ actions.hideResult }/> + onFocus={ actions.selectInput }/>
    { showResult && !isLoading && From b35fed9d13ce1416c730b0b0e8a066d374581a2e Mon Sep 17 00:00:00 2001 From: Asuka Ochi Date: Thu, 24 Jan 2019 13:17:17 +0000 Subject: [PATCH 35/71] Disable input box's autocomplete (#237) The autocomplete list overlaps x-topic-search suggestion list. --- .../__tests__/__snapshots__/x-topic-search.test.jsx.snap | 3 +++ components/x-topic-search/src/TopicSearch.jsx | 1 + 2 files changed, 4 insertions(+) diff --git a/components/x-topic-search/__tests__/__snapshots__/x-topic-search.test.jsx.snap b/components/x-topic-search/__tests__/__snapshots__/x-topic-search.test.jsx.snap index 459b2312e..5b60a8eeb 100644 --- a/components/x-topic-search/__tests__/__snapshots__/x-topic-search.test.jsx.snap +++ b/components/x-topic-search/__tests__/__snapshots__/x-topic-search.test.jsx.snap @@ -22,6 +22,7 @@ exports[`x-topic-search given all topics include search term are followed should class="TopicSearch_search-icon__2proG" /> From 1f771ef52b6318f861a2540ea4835f1310f3f482 Mon Sep 17 00:00:00 2001 From: fenglish Date: Wed, 23 Jan 2019 17:26:05 +0000 Subject: [PATCH 36/71] Pass followedTopicIds into SuggestionList to update FollowButton There were no triggers to update internal x-follow-buttons' isFollowed state. It displays just added topics with `added` follow buttons in the search result by Passing followedTopicIds as a SuggestionList's prop and comparing them to suggestion ids. --- components/x-topic-search/src/SuggestionList.jsx | 8 ++++++-- components/x-topic-search/src/TopicSearch.jsx | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/components/x-topic-search/src/SuggestionList.jsx b/components/x-topic-search/src/SuggestionList.jsx index d76d78b41..31211e851 100644 --- a/components/x-topic-search/src/SuggestionList.jsx +++ b/components/x-topic-search/src/SuggestionList.jsx @@ -3,7 +3,7 @@ import { FollowButton } from '@financial-times/x-follow-button'; import styles from './TopicSearch.scss'; import classNames from 'classnames'; -export default ({ suggestions, searchTerm, csrfToken }) => { +export default ({ suggestions, searchTerm, csrfToken, followedTopicIds = [] }) => { const listResults = suggestions.map((suggestion, index) => ( @@ -19,7 +19,11 @@ export default ({ suggestions, searchTerm, csrfToken }) => { { suggestion.prefLabel } - + diff --git a/components/x-topic-search/src/TopicSearch.jsx b/components/x-topic-search/src/TopicSearch.jsx index 0de842a88..8562da67e 100644 --- a/components/x-topic-search/src/TopicSearch.jsx +++ b/components/x-topic-search/src/TopicSearch.jsx @@ -61,7 +61,7 @@ const topicSearchActions = withActions(({ minSearchLength = 2, maxSuggestions = } })); -const TopicSearch = topicSearchActions(({ searchTerm, showResult, result, actions, isLoading, csrfToken }) => ( +const TopicSearch = topicSearchActions(({ searchTerm, showResult, result, actions, isLoading, csrfToken, followedTopicIds }) => (

    Search for topics, authors, companies, or other areas of interest @@ -88,7 +88,7 @@ const TopicSearch = topicSearchActions(({ searchTerm, showResult, result, action
    { result.unfollowedSuggestions.length > 0 && - } + } { !result.unfollowedSuggestions.length && result.followedSuggestions.length > 0 && } From 7619cbe39391106e44e3d63d720c7ee2191e3188 Mon Sep 17 00:00:00 2001 From: fenglish Date: Thu, 24 Jan 2019 09:47:25 +0000 Subject: [PATCH 37/71] Apply suggestion mapping directly --- components/x-topic-search/src/SuggestionList.jsx | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/components/x-topic-search/src/SuggestionList.jsx b/components/x-topic-search/src/SuggestionList.jsx index 31211e851..d056b34e2 100644 --- a/components/x-topic-search/src/SuggestionList.jsx +++ b/components/x-topic-search/src/SuggestionList.jsx @@ -3,10 +3,10 @@ import { FollowButton } from '@financial-times/x-follow-button'; import styles from './TopicSearch.scss'; import classNames from 'classnames'; -export default ({ suggestions, searchTerm, csrfToken, followedTopicIds = [] }) => { - - const listResults = suggestions.map((suggestion, index) => ( +export default ({ suggestions, searchTerm, csrfToken, followedTopicIds = [] }) => ( +
      + { suggestions.map((suggestion, index) => (
    • + ))} - )) - - return ( -
        - { listResults } -
      ); -}; +
    +); From 5bb4eed3ed65edb77e022cda592ae282f6cb19f6 Mon Sep 17 00:00:00 2001 From: fenglish Date: Thu, 24 Jan 2019 10:25:22 +0000 Subject: [PATCH 38/71] Move the decision of displaying result to one place --- components/x-topic-search/src/AllFollowed.jsx | 32 ------------ .../x-topic-search/src/ResultContainer.jsx | 51 +++++++++++++++++++ components/x-topic-search/src/TopicSearch.jsx | 20 +++----- 3 files changed, 57 insertions(+), 46 deletions(-) delete mode 100644 components/x-topic-search/src/AllFollowed.jsx create mode 100644 components/x-topic-search/src/ResultContainer.jsx diff --git a/components/x-topic-search/src/AllFollowed.jsx b/components/x-topic-search/src/AllFollowed.jsx deleted file mode 100644 index 851aa70d9..000000000 --- a/components/x-topic-search/src/AllFollowed.jsx +++ /dev/null @@ -1,32 +0,0 @@ -import { h } from '@financial-times/x-engine'; -import styles from './TopicSearch.scss'; -import classNames from 'classnames'; - -// transform like this => topic1, topic2 and topic3 -const arrayToSentence = followedSuggestions => { - const topicsLength = followedSuggestions.length; - - if (topicsLength === 1) { - return { followedSuggestions[0].prefLabel }; - } else { - return followedSuggestions - .map((topic, index) => { - if (index === topicsLength - 1) { - // the last topic - return and { topic.prefLabel } - } else if (index === topicsLength - 2) { - // one before the last topic - return { topic.prefLabel } ; - } else { - return { topic.prefLabel }, ; - } - }) - } - -}; - -export default ({ followedSuggestions }) => ( -
    - You already follow { arrayToSentence(followedSuggestions) } -
    -); diff --git a/components/x-topic-search/src/ResultContainer.jsx b/components/x-topic-search/src/ResultContainer.jsx new file mode 100644 index 000000000..c7b1e6965 --- /dev/null +++ b/components/x-topic-search/src/ResultContainer.jsx @@ -0,0 +1,51 @@ +import { h } from '@financial-times/x-engine'; +import styles from './TopicSearch.scss'; +import classNames from 'classnames'; + +import SuggestionList from './SuggestionList'; +import NoSuggestions from './NoSuggestions'; + +// transform like this => topic1, topic2 and topic3 +const arrayToSentence = followedSuggestions => { + const topicsLength = followedSuggestions.length; + + if (topicsLength === 1) { + return { followedSuggestions[0].prefLabel }; + } else { + return followedSuggestions + .map((topic, index) => { + if (index === topicsLength - 1) { + // the last topic + return and { topic.prefLabel } + } else if (index === topicsLength - 2) { + // one before the last topic + return { topic.prefLabel } ; + } else { + return { topic.prefLabel }, ; + } + }) + } + +}; + +export default ({ result, searchTerm, csrfToken, followedTopicIds }) => ( + +
    + + { result.unfollowedSuggestions.length > 0 && + } + + { !result.unfollowedSuggestions.length && result.followedSuggestions.length > 0 && +
    + You already follow { arrayToSentence(result.followedSuggestions) } +
    } + + { !result.unfollowedSuggestions.length && !result.followedSuggestions.length && + } + +
    +); diff --git a/components/x-topic-search/src/TopicSearch.jsx b/components/x-topic-search/src/TopicSearch.jsx index 8562da67e..4a5e93a9c 100644 --- a/components/x-topic-search/src/TopicSearch.jsx +++ b/components/x-topic-search/src/TopicSearch.jsx @@ -5,9 +5,7 @@ import classNames from 'classnames'; import getSuggestions from './lib/get-suggestions.js'; import debounce from 'debounce-promise'; -import SuggestionList from './SuggestionList'; -import NoSuggestions from './NoSuggestions'; -import AllFollowed from './AllFollowed'; +import ResultContainer from './ResultContainer'; const debounceGetSuggestions = debounce(getSuggestions, 150); @@ -85,17 +83,11 @@ const TopicSearch = topicSearchActions(({ searchTerm, showResult, result, action
    { showResult && !isLoading && -
    - - { result.unfollowedSuggestions.length > 0 && - } - - { !result.unfollowedSuggestions.length && result.followedSuggestions.length > 0 && - } - - { !result.unfollowedSuggestions.length && !result.followedSuggestions.length && - } -
    } + }

    )); From 7ff55ad6264f6fd231bc83b872e603e16678fb64 Mon Sep 17 00:00:00 2001 From: fenglish Date: Thu, 24 Jan 2019 11:34:44 +0000 Subject: [PATCH 39/71] Make the condition to display result more readable --- .../x-topic-search/src/ResultContainer.jsx | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/components/x-topic-search/src/ResultContainer.jsx b/components/x-topic-search/src/ResultContainer.jsx index c7b1e6965..9fd251ba0 100644 --- a/components/x-topic-search/src/ResultContainer.jsx +++ b/components/x-topic-search/src/ResultContainer.jsx @@ -28,24 +28,30 @@ const arrayToSentence = followedSuggestions => { }; -export default ({ result, searchTerm, csrfToken, followedTopicIds }) => ( -
    +export default ({ result, searchTerm, csrfToken, followedTopicIds }) => { - { result.unfollowedSuggestions.length > 0 && + const hasFollowedSuggestions = result.followedSuggestions.length > 0; + const hasUnfollowedSuggestions = result.unfollowedSuggestions.length > 0; + + return ( +
    + + { hasUnfollowedSuggestions && } + suggestions={ result.unfollowedSuggestions } + searchTerm={ searchTerm } + csrfToken={ csrfToken } + followedTopicIds={ followedTopicIds }/> } - { !result.unfollowedSuggestions.length && result.followedSuggestions.length > 0 && + { !hasUnfollowedSuggestions && hasFollowedSuggestions &&
    - You already follow { arrayToSentence(result.followedSuggestions) } + You already follow { arrayToSentence(result.followedSuggestions) }
    } - { !result.unfollowedSuggestions.length && !result.followedSuggestions.length && + { !hasUnfollowedSuggestions && !hasFollowedSuggestions && } -
    -); +
    + ); +}; From 43ae068154190947e40a47b4e8b2a5ac10eaaa75 Mon Sep 17 00:00:00 2001 From: fenglish Date: Thu, 24 Jan 2019 13:32:37 +0000 Subject: [PATCH 40/71] update x-follow-button to 0.0.11 --- components/x-topic-search/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/x-topic-search/package.json b/components/x-topic-search/package.json index 76e023a9e..fd441f3b8 100644 --- a/components/x-topic-search/package.json +++ b/components/x-topic-search/package.json @@ -18,7 +18,7 @@ "license": "ISC", "dependencies": { "@financial-times/x-engine": "file:../../packages/x-engine", - "@financial-times/x-follow-button": "0.0.9", + "@financial-times/x-follow-button": "0.0.11", "@financial-times/x-interaction": "^1.0.0-beta.6", "classnames": "^2.2.6", "debounce-promise": "^3.1.0" From 1617c232666739f611d11a2d33f45d2adec99532 Mon Sep 17 00:00:00 2001 From: fenglish Date: Tue, 29 Jan 2019 14:06:04 +0000 Subject: [PATCH 41/71] Remove unnecessary data-component="topic-search" --- .../__tests__/__snapshots__/x-topic-search.test.jsx.snap | 3 --- components/x-topic-search/src/ResultContainer.jsx | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/components/x-topic-search/__tests__/__snapshots__/x-topic-search.test.jsx.snap b/components/x-topic-search/__tests__/__snapshots__/x-topic-search.test.jsx.snap index 5b60a8eeb..5b1f2cf5d 100644 --- a/components/x-topic-search/__tests__/__snapshots__/x-topic-search.test.jsx.snap +++ b/components/x-topic-search/__tests__/__snapshots__/x-topic-search.test.jsx.snap @@ -32,7 +32,6 @@ exports[`x-topic-search given all topics include search term are followed should
      { const hasUnfollowedSuggestions = result.unfollowedSuggestions.length > 0; return ( -
      +
      { hasUnfollowedSuggestions && Date: Tue, 29 Jan 2019 17:17:45 +0000 Subject: [PATCH 42/71] Apply custom styles to search cancel button (#242) The search cancel button is supported only in WebKit and Blink, hence the vendor prefix is needed. --- components/x-topic-search/src/TopicSearch.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/components/x-topic-search/src/TopicSearch.scss b/components/x-topic-search/src/TopicSearch.scss index d9880361a..bca94f69d 100644 --- a/components/x-topic-search/src/TopicSearch.scss +++ b/components/x-topic-search/src/TopicSearch.scss @@ -42,6 +42,11 @@ &::placeholder { color: oColorsGetPaletteColor('white'); } + + &::-webkit-search-cancel-button { + @include oIconsGetIcon('cross', oColorsGetPaletteColor('white'), 26); + -webkit-appearance: none; + } } .result-container { From a3e8cab90da51990b797f874d7e97aae3526a25c Mon Sep 17 00:00:00 2001 From: Asuka Ochi Date: Wed, 30 Jan 2019 11:51:05 +0000 Subject: [PATCH 43/71] Change testing style from snapshots tests to unit tests (#228) --- .../x-topic-search.test.jsx.snap | 235 ------------------ .../__tests__/x-topic-search.test.jsx | 154 +++++++++--- 2 files changed, 117 insertions(+), 272 deletions(-) delete mode 100644 components/x-topic-search/__tests__/__snapshots__/x-topic-search.test.jsx.snap diff --git a/components/x-topic-search/__tests__/__snapshots__/x-topic-search.test.jsx.snap b/components/x-topic-search/__tests__/__snapshots__/x-topic-search.test.jsx.snap deleted file mode 100644 index 5b1f2cf5d..000000000 --- a/components/x-topic-search/__tests__/__snapshots__/x-topic-search.test.jsx.snap +++ /dev/null @@ -1,235 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`x-topic-search given all topics include search term are followed should render followed topics name list 1`] = ` -
      -

      - Search for topics, authors, companies, or other areas of interest -

      - -
      - - -
      -
      -
      - You already follow - - - abcd - - , - - - - abcde - - - - - and - - abcdef - - -
      -
      -
      -`; - -exports[`x-topic-search given there are unfollowed topics include search term should render the unfollowed topics list with x-follow-button 1`] = ` -
      -

      - Search for topics, authors, companies, or other areas of interest -

      - -
      - - -
      -
      - -
      -
      -`; - -exports[`x-topic-search given there is no topics include search term exist should render no topics message 1`] = ` -
      -

      - Search for topics, authors, companies, or other areas of interest -

      - -
      - - -
      -
      -
      -

      - No topics matching - - abc - -

      -

      - Suggestions: -

      -
        -
      • - Make sure that all words are spelled correctly. -
      • -
      • - Try different keywords. -
      • -
      • - Try more general keywords. -
      • -
      • - Try fewer keywords. -
      • -
      -
      -
      -
      -`; diff --git a/components/x-topic-search/__tests__/x-topic-search.test.jsx b/components/x-topic-search/__tests__/x-topic-search.test.jsx index c4f84a6bb..c77dc3a30 100644 --- a/components/x-topic-search/__tests__/x-topic-search.test.jsx +++ b/components/x-topic-search/__tests__/x-topic-search.test.jsx @@ -1,66 +1,146 @@ +const fetchMock = require('fetch-mock'); const { h } = require('@financial-times/x-engine'); const { mount } = require('@financial-times/x-test-utils/enzyme'); const { TopicSearch } = require('../'); -const fetchMock = require('fetch-mock'); +const searchTerm = 'Dog'; +const searchTermNoResult = 'Blobfish'; +const searchTermAllFollowed = 'Cat'; +const minSearchLength = 3; const maxSuggestions = 3; -const searchTerm = 'abc'; const apiUrl = 'api-url'; -const apiUrlWithQueries = `${ apiUrl }?count=${ maxSuggestions }&partial=${ searchTerm }`; -const props = { apiUrl, maxSuggestions }; - -const topicOne = { id: 'TOPIC-1__id', prefLabel: `${searchTerm}d`, url:'TOPIC-1__url' }; -const topicTwo = { id: 'TOPIC-2__id', prefLabel: `${searchTerm}de`, url:'TOPIC-2__url' }; -const topicThree = { id: 'TOPIC-3__id', prefLabel: `${searchTerm}def`, url:'TOPIC-3__url' }; +const alreadyFollowedTopics = [ + { uuid: 'Cat-House-id', name: 'Cat House' }, + { uuid: 'Cat-Food-id', name: 'Cat Food' }, + { uuid: 'Cat-Toys-id', name: 'Cat Toys' } +]; +const buildSearchUrl = term => `${apiUrl}?count=${maxSuggestions}&partial=${term}`; describe('x-topic-search', () => { - - let fetchUrl; + const waitForApiResponse = () => new Promise(resolve => { setTimeout(resolve, 500); }); + let target; beforeEach(() => { - fetchUrl = apiUrlWithQueries; + const props = { + minSearchLength, + maxSuggestions, + apiUrl, + followedTopicIds: alreadyFollowedTopics.map(topic => topic.uuid), + }; + target = mount(); }); afterEach(() => { - fetchMock.restore(); + fetchMock.reset(); + }); + + describe('initial rendering', () => { + it('should render with input box', () => { + expect(target.find('input').exists()).toBe(true); + }); + + it('should not display result container', () => { + expect(target.render().children('div')).toHaveLength(1); + }); }); - describe('given there are unfollowed topics include search term', () => { - it('should render the unfollowed topics list with x-follow-button', async () => { - fetchMock.get(fetchUrl, [ topicOne, topicTwo ]); + describe('given inputted text is shorter than minSearchLength', () => { + it('should not render result', () => { + const wordLessThanMin = searchTerm.slice(0, minSearchLength - 1); + const apiUrlWithResults = buildSearchUrl(wordLessThanMin); - const subject = mount(); - const input = subject.find('input'); - await input.prop('onInput')({ target: { value: searchTerm }}); + fetchMock.get(apiUrlWithResults, []); - expect(subject.render()).toMatchSnapshot(); - }) + target.find('input').simulate('input', { target: { value: wordLessThanMin }}); + + return waitForApiResponse().then(() => { + expect(fetchMock.called(apiUrlWithResults)).toBe(false); + expect(target.render().children('div')).toHaveLength(1); + }); + }); }); - describe('given there is no topics include search term exist', () => { - it('should render no topics message', async () => { - fetchMock.get(fetchUrl, []); + describe('given searchTerm which has some topic suggestions to follow', () => { + const apiUrlWithResults = buildSearchUrl(searchTerm); + const topicSuggestions = [ + { id: 'Dog-House-id', prefLabel: 'Dog House', url: 'Dog-House-url' }, + { id: 'Dog-Food-id', prefLabel: 'Dog Food', url: 'Dog-Food-url' }, + { id: 'Dog-Toys-id', prefLabel: 'Dog Toys', url: 'Dog-Toys-url' } + ]; + + fetchMock.get(apiUrlWithResults, topicSuggestions); + + beforeEach(() => { + target.find('input').simulate('input', { target: { value: searchTerm } }); + + return waitForApiResponse(); + }); + + it('should render topics list with follow button', () => { + expect(fetchMock.called(apiUrlWithResults)).toBe(true); + expect(target.render().children('div')).toHaveLength(2); + + const suggestionsList = target.render().find('li'); - const subject = mount(); - const input = subject.find('input'); - await input.prop('onInput')({ target: { value: searchTerm }}); + expect(suggestionsList).toHaveLength(maxSuggestions); - expect(subject.render()).toMatchSnapshot(); - }) + topicSuggestions.forEach((topic, index) => { + const suggestion = suggestionsList.eq(index); + + expect(suggestion.find('a').text()).toEqual(topic.prefLabel); + expect(suggestion.find('a').attr('href')).toEqual(topic.url); + expect(suggestion.find('button')).toHaveLength(1); + }); + }); + }); + + describe('given searchTerm which has no topic suggestions to follow', () => { + const apiUrlNoResults = buildSearchUrl(searchTermNoResult); + + fetchMock.get(apiUrlNoResults, []); + + beforeEach(() => { + target.find('input').simulate('input', { target: { value: searchTermNoResult } }); + + return waitForApiResponse(); + }); + + it('should render no topic message', () => { + expect(fetchMock.called(apiUrlNoResults)).toBe(true); + + const resultContainer = target.render().children('div').eq(1); + + expect(resultContainer).toHaveLength(1); + expect(resultContainer.find('h2').text()).toMatch('No topics matching'); + }); }); - describe('given all topics include search term are followed', () => { - it('should render followed topics name list', async () => { - props.followedTopicIds = [ topicOne.id, topicTwo.id, topicThree.id ]; + describe('given searchTerm which all the topics has been followed', () => { + const apiUrlAllFollowed = buildSearchUrl(searchTermAllFollowed); + + fetchMock.get(apiUrlAllFollowed, alreadyFollowedTopics.map(topic => ({ + id: topic.uuid, + prefLabel: topic.name, + url: topic.name.replace(' ', '-') + }))); + + beforeEach(() => { + target.find('input').simulate('input', { target: { value: searchTermAllFollowed } }); + + return waitForApiResponse(); + }); - fetchMock.get(fetchUrl, [ topicOne, topicTwo, topicThree ]); + it('should render already followed message with name of the topics', () => { + expect(fetchMock.called(apiUrlAllFollowed)).toBe(true); - const subject = mount(); - const input = subject.find('input'); - await input.prop('onInput')({ target: { value: searchTerm }}); + const resultContainer = target.render().children('div').eq(1); - expect(subject.render()).toMatchSnapshot(); - }) + expect(resultContainer).toHaveLength(1); + expect(resultContainer.text()) + .toMatch( + `You already follow ${alreadyFollowedTopics[0].name}, ${alreadyFollowedTopics[1].name} and ${alreadyFollowedTopics[2].name}` + ); + }); }); }); From 7e5df9fccb7d7e97f1366d0cae622372783ceb69 Mon Sep 17 00:00:00 2001 From: dan-searle Date: Thu, 24 Jan 2019 13:48:07 +0000 Subject: [PATCH 44/71] Extend Component class instead of using x-interaction --- components/x-topic-search/package.json | 1 - .../x-topic-search/src/ResultContainer.jsx | 10 +- components/x-topic-search/src/TopicSearch.jsx | 154 +++++++++--------- 3 files changed, 84 insertions(+), 81 deletions(-) diff --git a/components/x-topic-search/package.json b/components/x-topic-search/package.json index fd441f3b8..22c52604e 100644 --- a/components/x-topic-search/package.json +++ b/components/x-topic-search/package.json @@ -19,7 +19,6 @@ "dependencies": { "@financial-times/x-engine": "file:../../packages/x-engine", "@financial-times/x-follow-button": "0.0.11", - "@financial-times/x-interaction": "^1.0.0-beta.6", "classnames": "^2.2.6", "debounce-promise": "^3.1.0" }, diff --git a/components/x-topic-search/src/ResultContainer.jsx b/components/x-topic-search/src/ResultContainer.jsx index d696567aa..d360cf75e 100644 --- a/components/x-topic-search/src/ResultContainer.jsx +++ b/components/x-topic-search/src/ResultContainer.jsx @@ -29,24 +29,24 @@ const arrayToSentence = followedSuggestions => { }; -export default ({ result, searchTerm, csrfToken, followedTopicIds }) => { +export default ({ followedSuggestions, searchTerm, csrfToken, followedTopicIds, unfollowedSuggestions }) => { - const hasFollowedSuggestions = result.followedSuggestions.length > 0; - const hasUnfollowedSuggestions = result.unfollowedSuggestions.length > 0; + const hasFollowedSuggestions = followedSuggestions.length > 0; + const hasUnfollowedSuggestions = unfollowedSuggestions.length > 0; return (
      { hasUnfollowedSuggestions && } { !hasUnfollowedSuggestions && hasFollowedSuggestions &&
      - You already follow { arrayToSentence(result.followedSuggestions) } + You already follow { arrayToSentence(followedSuggestions) }
      } { !hasUnfollowedSuggestions && !hasFollowedSuggestions && diff --git a/components/x-topic-search/src/TopicSearch.jsx b/components/x-topic-search/src/TopicSearch.jsx index 4a5e93a9c..cb00d8cbd 100644 --- a/components/x-topic-search/src/TopicSearch.jsx +++ b/components/x-topic-search/src/TopicSearch.jsx @@ -1,5 +1,4 @@ -import { h } from '@financial-times/x-engine'; -import { withActions } from '@financial-times/x-interaction'; +import { h, Component } from '@financial-times/x-engine'; import styles from './TopicSearch.scss'; import classNames from 'classnames'; import getSuggestions from './lib/get-suggestions.js'; @@ -7,89 +6,94 @@ import debounce from 'debounce-promise'; import ResultContainer from './ResultContainer'; -const debounceGetSuggestions = debounce(getSuggestions, 150); +class TopicSearch extends Component { + constructor(props) { + super(props); + + this.minSearchLength = props.minSearchLength || 2; + this.maxSuggestions = props.maxSuggestions || 5; + this.apiUrl = props.apiUrl; + this.getSuggestions = debounce(getSuggestions, 150); + this.handleInputChange = this.handleInputChange.bind(this); + this.handleInputClickOrFocus = this.handleInputClickOrFocus.bind(this); + + this.state = { + followedTopicIds: [], + searchTerm: '', + showResult: false, + followedSuggestions: [], + unFollowedSuggestions: [] + }; + } -let resultExists = false; + handleInputChange(event) { + const searchTerm = event.target.value.trim(); -const topicSearchActions = withActions(({ minSearchLength = 2, maxSuggestions = 5, apiUrl, followedTopicIds = [] }) => ({ - async checkInput(event) { - const searchTerm = event.target.value && event.target.value.trim(); + this.setState({ searchTerm }); - if (searchTerm.length >= minSearchLength) { - return debounceGetSuggestions(searchTerm, maxSuggestions, apiUrl, followedTopicIds) - .then(result => { - resultExists = true; - return { showResult: true, result, searchTerm }; + if (searchTerm.length >= this.minSearchLength) { + this.getSuggestions(searchTerm, this.maxSuggestions, this.apiUrl, this.state.followedTopicIds) + .then(({ followedSuggestions, unfollowedSuggestions }) => { + this.setState({ + followedSuggestions, + unfollowedSuggestions, + showResult: true + }); }) .catch(() => { - resultExists = false; - return { showResult: false }; + this.setState({ + showResult: false + }); }); } else { - resultExists = false; - return Promise.resolve({ showResult: false }); - } - }, - - topicFollowed (subjectId) { - if (!followedTopicIds.includes(subjectId)) { - followedTopicIds.push(subjectId); - } - - return { followedTopicIds }; - }, - - topicUnfollowed (subjectId) { - const targetIdIndex = followedTopicIds.indexOf(subjectId); - - if (targetIdIndex > -1) { - followedTopicIds.splice(targetIdIndex, 1); + this.setState({ + showResult: false + }); } - - return { followedTopicIds }; - }, - - selectInput (event) { - event.target.select(); - return { showResult: resultExists }; - }, - - hideResult() { - return { showResult: false }; } -})); - -const TopicSearch = topicSearchActions(({ searchTerm, showResult, result, actions, isLoading, csrfToken, followedTopicIds }) => ( -
      -

      - Search for topics, authors, companies, or other areas of interest -

      - -
      - - -
      - - { showResult && !isLoading && - } + handleInputClickOrFocus() { + console.log('handleInputClickOrFocus'); + // this.setState({ + // showResult: true + // }); + } -
      -)); + render() { + const { csrfToken, followedSuggestions, followedTopicIds, isLoading, searchTerm, showResult, unfollowedSuggestions } = this.state; + + return ( +
      +

      + Search for topics, authors, companies, or other areas of interest +

      + + +
      + + +
      + + { showResult && !isLoading && + } +
      + ); + } +} export { TopicSearch }; From 07e046749addac6e401dc7d9c780dc349514f0c3 Mon Sep 17 00:00:00 2001 From: dan-searle Date: Mon, 28 Jan 2019 16:50:56 +0000 Subject: [PATCH 45/71] followedTopicIds prop is expected to be updated when followed topics change. --- components/x-topic-search/src/TopicSearch.jsx | 43 ++++++++++++++----- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/components/x-topic-search/src/TopicSearch.jsx b/components/x-topic-search/src/TopicSearch.jsx index cb00d8cbd..284489135 100644 --- a/components/x-topic-search/src/TopicSearch.jsx +++ b/components/x-topic-search/src/TopicSearch.jsx @@ -14,25 +14,38 @@ class TopicSearch extends Component { this.maxSuggestions = props.maxSuggestions || 5; this.apiUrl = props.apiUrl; this.getSuggestions = debounce(getSuggestions, 150); + this.outsideEvents = ['focusout', 'focusin', 'click']; this.handleInputChange = this.handleInputChange.bind(this); this.handleInputClickOrFocus = this.handleInputClickOrFocus.bind(this); + this.handleInteractionOutside = this.handleInteractionOutside.bind(this); this.state = { - followedTopicIds: [], searchTerm: '', showResult: false, followedSuggestions: [], - unFollowedSuggestions: [] + unfollowedSuggestions: [] }; } + componentDidMount() { + this.outsideEvents.forEach(action => { + document.body.addEventListener(action, this.handleInteractionOutside); + }); + } + + componentWillUnmount() { + this.outsideEvents.forEach(action => { + document.body.removeEventListener(action, this.handleInteractionOutside); + }); + } + handleInputChange(event) { const searchTerm = event.target.value.trim(); this.setState({ searchTerm }); if (searchTerm.length >= this.minSearchLength) { - this.getSuggestions(searchTerm, this.maxSuggestions, this.apiUrl, this.state.followedTopicIds) + this.getSuggestions(searchTerm, this.maxSuggestions, this.apiUrl, this.props.followedTopicIds) .then(({ followedSuggestions, unfollowedSuggestions }) => { this.setState({ followedSuggestions, @@ -52,18 +65,28 @@ class TopicSearch extends Component { } } + handleInteractionOutside(event) { + if (!this.rootEl.contains(event.target)) { + this.setState({ + showResult: false + }); + } + } + handleInputClickOrFocus() { - console.log('handleInputClickOrFocus'); - // this.setState({ - // showResult: true - // }); + if (this.state.searchTerm.length >= this.minSearchLength) { + this.setState({ + showResult: true + }); + } } render() { - const { csrfToken, followedSuggestions, followedTopicIds, isLoading, searchTerm, showResult, unfollowedSuggestions } = this.state; + const { followedTopicIds } = this.props; + const { csrfToken, followedSuggestions, searchTerm, showResult, unfollowedSuggestions } = this.state; return ( -
      +
      this.rootEl = el}>

      Search for topics, authors, companies, or other areas of interest

      @@ -84,7 +107,7 @@ class TopicSearch extends Component { />
      - { showResult && !isLoading && + { showResult && Date: Wed, 30 Jan 2019 15:57:00 +0000 Subject: [PATCH 46/71] Create story knob for followedTopicIds prop. --- components/x-topic-search/stories/index.js | 3 +++ components/x-topic-search/stories/knobs.js | 19 +++++++++++++++++++ .../x-topic-search/stories/topic-search.js | 6 +++++- 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 components/x-topic-search/stories/knobs.js diff --git a/components/x-topic-search/stories/index.js b/components/x-topic-search/stories/index.js index 419ff844a..601676cb3 100644 --- a/components/x-topic-search/stories/index.js +++ b/components/x-topic-search/stories/index.js @@ -15,3 +15,6 @@ exports.dependencies = { exports.stories = [ require('./topic-search') ]; + +exports.knobs = require('./knobs'); + diff --git a/components/x-topic-search/stories/knobs.js b/components/x-topic-search/stories/knobs.js new file mode 100644 index 000000000..7809db53e --- /dev/null +++ b/components/x-topic-search/stories/knobs.js @@ -0,0 +1,19 @@ +module.exports = (data, { select }) => { + return { + followedTopicIds() { + return select( + 'Followed Topics', + { + None: [], + 'World Elephant Water Polo': ['f95d1e16-2307-4feb-b3ff-6f224798aa49'], + 'Brexit, Brexit Briefing, Brexit Unspun Podcast': [ + '19b95057-4614-45fb-9306-4d54049354db', + '464cc2f2-395e-4c36-bb29-01727fc95558', + 'c4e899ed-157e-4446-86f0-5a65803dc07a' + ] + }, + [] + ); + } + }; +}; diff --git a/components/x-topic-search/stories/topic-search.js b/components/x-topic-search/stories/topic-search.js index 76f3536de..387b4f604 100644 --- a/components/x-topic-search/stories/topic-search.js +++ b/components/x-topic-search/stories/topic-search.js @@ -1,6 +1,6 @@ exports.title = 'Topic Search Bar'; -exports.data = { +const data = { minSearchLength: 2, maxSuggestions: 10, apiUrl: '//tag-facets-api.ft.com/annotations', @@ -10,6 +10,10 @@ exports.data = { csrfToken: 'csrfToken' }; +exports.data = data; + +exports.knobs = Object.keys(data); + // This reference is only required for hot module loading in development // exports.m = module; From 9e1f5a6ef0fba50d82e93c2ff3312ea92b2e03b2 Mon Sep 17 00:00:00 2001 From: dan-searle Date: Wed, 30 Jan 2019 16:27:26 +0000 Subject: [PATCH 47/71] Minor improvement to arrayToSentence function. --- .../x-topic-search/src/ResultContainer.jsx | 48 +++++++------------ .../x-topic-search/src/lib/get-suggestions.js | 8 ++-- 2 files changed, 21 insertions(+), 35 deletions(-) diff --git a/components/x-topic-search/src/ResultContainer.jsx b/components/x-topic-search/src/ResultContainer.jsx index d360cf75e..cc6906168 100644 --- a/components/x-topic-search/src/ResultContainer.jsx +++ b/components/x-topic-search/src/ResultContainer.jsx @@ -9,49 +9,37 @@ import NoSuggestions from './NoSuggestions'; const arrayToSentence = followedSuggestions => { const topicsLength = followedSuggestions.length; - if (topicsLength === 1) { - return { followedSuggestions[0].prefLabel }; - } else { - return followedSuggestions - .map((topic, index) => { - if (index === topicsLength - 1) { - // the last topic - return and { topic.prefLabel } - } else if (index === topicsLength - 2) { - // one before the last topic - return { topic.prefLabel } ; - } else { - return { topic.prefLabel }, ; - } - }) - } - + return followedSuggestions.map((topic, index) => ( + + {topicsLength > 1 && index === topicsLength - 1 && ' and '} + {topic.prefLabel} + {index < topicsLength - 2 && ', '} + + )); }; export default ({ followedSuggestions, searchTerm, csrfToken, followedTopicIds, unfollowedSuggestions }) => { - const hasFollowedSuggestions = followedSuggestions.length > 0; const hasUnfollowedSuggestions = unfollowedSuggestions.length > 0; return (
      - - { hasUnfollowedSuggestions && + {hasUnfollowedSuggestions && } + suggestions={ unfollowedSuggestions } + searchTerm={ searchTerm } + csrfToken={ csrfToken } + followedTopicIds={ followedTopicIds } + />} - { !hasUnfollowedSuggestions && hasFollowedSuggestions && + {!hasUnfollowedSuggestions && hasFollowedSuggestions &&
      - You already follow { arrayToSentence(followedSuggestions) } -
      } - - { !hasUnfollowedSuggestions && !hasFollowedSuggestions && - } + You already follow { arrayToSentence(followedSuggestions) } +
      } + {!hasUnfollowedSuggestions && !hasFollowedSuggestions && + }
      ); }; diff --git a/components/x-topic-search/src/lib/get-suggestions.js b/components/x-topic-search/src/lib/get-suggestions.js index 4e1ea8e02..ec21c44ce 100644 --- a/components/x-topic-search/src/lib/get-suggestions.js +++ b/components/x-topic-search/src/lib/get-suggestions.js @@ -8,10 +8,9 @@ const separateFollowedAndUnfollowed = (suggestions = [], followedTopicIds) => { const unfollowedSuggestions = suggestions.filter(suggestion => !followedTopicIds.includes(suggestion.id)); return { followedSuggestions, unfollowedSuggestions }; -} +}; export default (searchTerm, maxSuggestions, apiUrl, followedTopicIds) => { - const dataSrc = addQueryParamToUrl('count', maxSuggestions, apiUrl, false); const url = addQueryParamToUrl('partial', searchTerm.replace(' ', '+'), dataSrc); @@ -25,8 +24,7 @@ export default (searchTerm, maxSuggestions, apiUrl, followedTopicIds) => { .then(suggestions => { return separateFollowedAndUnfollowed(suggestions, followedTopicIds) }) - .catch(() => { - throw new Error(); + .catch((err) => { + throw new Error(err); }); - }; From 31126f5393a5fa3d72bb39e124a952d723ec621f Mon Sep 17 00:00:00 2001 From: dan-searle Date: Wed, 30 Jan 2019 16:43:04 +0000 Subject: [PATCH 48/71] Ensure results are only displayed if they are for the current search term (defends against slow API response causing results to be shown when input is now empty). --- components/x-topic-search/src/TopicSearch.jsx | 9 ++++++--- components/x-topic-search/src/lib/get-suggestions.js | 9 ++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/components/x-topic-search/src/TopicSearch.jsx b/components/x-topic-search/src/TopicSearch.jsx index 284489135..fc95586bb 100644 --- a/components/x-topic-search/src/TopicSearch.jsx +++ b/components/x-topic-search/src/TopicSearch.jsx @@ -46,8 +46,9 @@ class TopicSearch extends Component { if (searchTerm.length >= this.minSearchLength) { this.getSuggestions(searchTerm, this.maxSuggestions, this.apiUrl, this.props.followedTopicIds) - .then(({ followedSuggestions, unfollowedSuggestions }) => { + .then(({ resultsForTerm, followedSuggestions, unfollowedSuggestions }) => { this.setState({ + resultsForTerm, followedSuggestions, unfollowedSuggestions, showResult: true @@ -55,11 +56,13 @@ class TopicSearch extends Component { }) .catch(() => { this.setState({ + resultsForTerm: null, showResult: false }); }); } else { this.setState({ + resultsForTerm: null, showResult: false }); } @@ -83,7 +86,7 @@ class TopicSearch extends Component { render() { const { followedTopicIds } = this.props; - const { csrfToken, followedSuggestions, searchTerm, showResult, unfollowedSuggestions } = this.state; + const { csrfToken, followedSuggestions, resultsForTerm, searchTerm, showResult, unfollowedSuggestions } = this.state; return (
      this.rootEl = el}> @@ -107,7 +110,7 @@ class TopicSearch extends Component { />
      - { showResult && + { showResult && resultsForTerm === searchTerm && { const queryParam = `${name}=${value}`; + return append === true ? `${url}&${queryParam}` : `${url}?${queryParam}`; }; @@ -19,11 +20,13 @@ export default (searchTerm, maxSuggestions, apiUrl, followedTopicIds) => { if (!response.ok) { throw new Error(response.statusText); } + return response.json(); }) - .then(suggestions => { - return separateFollowedAndUnfollowed(suggestions, followedTopicIds) - }) + .then(suggestions => ({ + resultsForTerm: searchTerm, + ...separateFollowedAndUnfollowed(suggestions, followedTopicIds) + })) .catch((err) => { throw new Error(err); }); From 014d87b524f8c9597c24e936b9efab489dcfe6ee Mon Sep 17 00:00:00 2001 From: dan-searle Date: Thu, 31 Jan 2019 10:12:54 +0000 Subject: [PATCH 49/71] Simplify get-suggestions response processing. --- components/x-topic-search/src/lib/get-suggestions.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/components/x-topic-search/src/lib/get-suggestions.js b/components/x-topic-search/src/lib/get-suggestions.js index 17b33a398..881b8fefe 100644 --- a/components/x-topic-search/src/lib/get-suggestions.js +++ b/components/x-topic-search/src/lib/get-suggestions.js @@ -4,13 +4,6 @@ const addQueryParamToUrl = (name, value, url, append = true) => { return append === true ? `${url}&${queryParam}` : `${url}?${queryParam}`; }; -const separateFollowedAndUnfollowed = (suggestions = [], followedTopicIds) => { - const followedSuggestions = suggestions.filter(suggestion => followedTopicIds.includes(suggestion.id)); - const unfollowedSuggestions = suggestions.filter(suggestion => !followedTopicIds.includes(suggestion.id)); - - return { followedSuggestions, unfollowedSuggestions }; -}; - export default (searchTerm, maxSuggestions, apiUrl, followedTopicIds) => { const dataSrc = addQueryParamToUrl('count', maxSuggestions, apiUrl, false); const url = addQueryParamToUrl('partial', searchTerm.replace(' ', '+'), dataSrc); @@ -25,7 +18,8 @@ export default (searchTerm, maxSuggestions, apiUrl, followedTopicIds) => { }) .then(suggestions => ({ resultsForTerm: searchTerm, - ...separateFollowedAndUnfollowed(suggestions, followedTopicIds) + followedSuggestions: suggestions.filter(suggestion => followedTopicIds.includes(suggestion.id)), + unfollowedSuggestions: suggestions.filter(suggestion => !followedTopicIds.includes(suggestion.id)) })) .catch((err) => { throw new Error(err); From 447dea88f6b00b5ec95472baf2cb0f580c521a20 Mon Sep 17 00:00:00 2001 From: dan-searle Date: Thu, 31 Jan 2019 12:09:20 +0000 Subject: [PATCH 50/71] Slightly more descriptive tests. --- .../__tests__/x-topic-search.test.jsx | 68 +++++++++---------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/components/x-topic-search/__tests__/x-topic-search.test.jsx b/components/x-topic-search/__tests__/x-topic-search.test.jsx index c77dc3a30..5bc356fbc 100644 --- a/components/x-topic-search/__tests__/x-topic-search.test.jsx +++ b/components/x-topic-search/__tests__/x-topic-search.test.jsx @@ -14,10 +14,14 @@ const alreadyFollowedTopics = [ { uuid: 'Cat-Food-id', name: 'Cat Food' }, { uuid: 'Cat-Toys-id', name: 'Cat Toys' } ]; -const buildSearchUrl = term => `${apiUrl}?count=${maxSuggestions}&partial=${term}`; describe('x-topic-search', () => { - const waitForApiResponse = () => new Promise(resolve => { setTimeout(resolve, 500); }); + const buildSearchUrl = term => `${apiUrl}?count=${maxSuggestions}&partial=${term}`; + const enterSearchTerm = searchTerm => { + target.find('input').simulate('input', { target: { value: searchTerm }}); + + return new Promise(resolve => { setTimeout(resolve, 400); }); + }; let target; beforeEach(() => { @@ -45,53 +49,54 @@ describe('x-topic-search', () => { }); describe('given inputted text is shorter than minSearchLength', () => { - it('should not render result', () => { - const wordLessThanMin = searchTerm.slice(0, minSearchLength - 1); - const apiUrlWithResults = buildSearchUrl(wordLessThanMin); + const apiUrlWithResults = buildSearchUrl('a'); - fetchMock.get(apiUrlWithResults, []); + fetchMock.get(apiUrlWithResults, []); - target.find('input').simulate('input', { target: { value: wordLessThanMin }}); + beforeEach(() => { + return enterSearchTerm('a'); + }); - return waitForApiResponse().then(() => { - expect(fetchMock.called(apiUrlWithResults)).toBe(false); - expect(target.render().children('div')).toHaveLength(1); - }); + it('does not make a request to the api or render any result', () => { + expect(fetchMock.called(apiUrlWithResults)).toBe(false); + expect(target.render().children('div')).toHaveLength(1); }); }); describe('given searchTerm which has some topic suggestions to follow', () => { const apiUrlWithResults = buildSearchUrl(searchTerm); - const topicSuggestions = [ + const unfollowedTopicSuggestions = [ { id: 'Dog-House-id', prefLabel: 'Dog House', url: 'Dog-House-url' }, { id: 'Dog-Food-id', prefLabel: 'Dog Food', url: 'Dog-Food-url' }, { id: 'Dog-Toys-id', prefLabel: 'Dog Toys', url: 'Dog-Toys-url' } ]; - fetchMock.get(apiUrlWithResults, topicSuggestions); + fetchMock.get(apiUrlWithResults, unfollowedTopicSuggestions); beforeEach(() => { - target.find('input').simulate('input', { target: { value: searchTerm } }); - - return waitForApiResponse(); + return enterSearchTerm(searchTerm); }); - it('should render topics list with follow button', () => { + it('requests the topic suggestions with count set to maxSuggestions', () => { expect(fetchMock.called(apiUrlWithResults)).toBe(true); + }); + + it('renders no more than the max number of suggestions', () => { expect(target.render().children('div')).toHaveLength(2); + expect(target.render().find('li')).toHaveLength(maxSuggestions); + }); + it('renders links and follow buttons for each suggestion', () => { const suggestionsList = target.render().find('li'); - expect(suggestionsList).toHaveLength(maxSuggestions); - - topicSuggestions.forEach((topic, index) => { + unfollowedTopicSuggestions.forEach((topic, index) => { const suggestion = suggestionsList.eq(index); expect(suggestion.find('a').text()).toEqual(topic.prefLabel); expect(suggestion.find('a').attr('href')).toEqual(topic.url); expect(suggestion.find('button')).toHaveLength(1); }); - }); + }) }); describe('given searchTerm which has no topic suggestions to follow', () => { @@ -100,12 +105,10 @@ describe('x-topic-search', () => { fetchMock.get(apiUrlNoResults, []); beforeEach(() => { - target.find('input').simulate('input', { target: { value: searchTermNoResult } }); - - return waitForApiResponse(); + return enterSearchTerm(searchTermNoResult); }); - it('should render no topic message', () => { + it('requests from the api and renders the no matching topics message', () => { expect(fetchMock.called(apiUrlNoResults)).toBe(true); const resultContainer = target.render().children('div').eq(1); @@ -115,7 +118,7 @@ describe('x-topic-search', () => { }); }); - describe('given searchTerm which all the topics has been followed', () => { + describe('given searchTerm which results in all suggestions already followed', () => { const apiUrlAllFollowed = buildSearchUrl(searchTermAllFollowed); fetchMock.get(apiUrlAllFollowed, alreadyFollowedTopics.map(topic => ({ @@ -125,22 +128,19 @@ describe('x-topic-search', () => { }))); beforeEach(() => { - target.find('input').simulate('input', { target: { value: searchTermAllFollowed } }); - - return waitForApiResponse(); + return enterSearchTerm(searchTermAllFollowed); }); - it('should render already followed message with name of the topics', () => { + it('requests the suggestions from the api', () => { expect(fetchMock.called(apiUrlAllFollowed)).toBe(true); + }); + it('renders the "already followed" message with names of the topics', () => { const resultContainer = target.render().children('div').eq(1); expect(resultContainer).toHaveLength(1); expect(resultContainer.text()) - .toMatch( - `You already follow ${alreadyFollowedTopics[0].name}, ${alreadyFollowedTopics[1].name} and ${alreadyFollowedTopics[2].name}` - ); + .toMatch(`You already follow ${alreadyFollowedTopics[0].name}, ${alreadyFollowedTopics[1].name} and ${alreadyFollowedTopics[2].name}`); }); }); - }); From 97be6f1489083dc6e59f0e50d6b94004b4fb933b Mon Sep 17 00:00:00 2001 From: dan-searle Date: Thu, 31 Jan 2019 14:20:40 +0000 Subject: [PATCH 51/71] Remove redundant throw in catch. --- components/x-topic-search/src/lib/get-suggestions.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/components/x-topic-search/src/lib/get-suggestions.js b/components/x-topic-search/src/lib/get-suggestions.js index 881b8fefe..f2e89c1c7 100644 --- a/components/x-topic-search/src/lib/get-suggestions.js +++ b/components/x-topic-search/src/lib/get-suggestions.js @@ -20,8 +20,5 @@ export default (searchTerm, maxSuggestions, apiUrl, followedTopicIds) => { resultsForTerm: searchTerm, followedSuggestions: suggestions.filter(suggestion => followedTopicIds.includes(suggestion.id)), unfollowedSuggestions: suggestions.filter(suggestion => !followedTopicIds.includes(suggestion.id)) - })) - .catch((err) => { - throw new Error(err); - }); + })); }; From b5996bb3ec373409d829d1d9a03e0ab87d45fe3c Mon Sep 17 00:00:00 2001 From: dan-searle Date: Thu, 31 Jan 2019 15:38:54 +0000 Subject: [PATCH 52/71] Bug fix: csrfToken is a prop not state... --- components/x-topic-search/src/TopicSearch.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/x-topic-search/src/TopicSearch.jsx b/components/x-topic-search/src/TopicSearch.jsx index fc95586bb..6cc5ef522 100644 --- a/components/x-topic-search/src/TopicSearch.jsx +++ b/components/x-topic-search/src/TopicSearch.jsx @@ -85,8 +85,8 @@ class TopicSearch extends Component { } render() { - const { followedTopicIds } = this.props; - const { csrfToken, followedSuggestions, resultsForTerm, searchTerm, showResult, unfollowedSuggestions } = this.state; + const { csrfToken, followedTopicIds } = this.props; + const { followedSuggestions, resultsForTerm, searchTerm, showResult, unfollowedSuggestions } = this.state; return (
      this.rootEl = el}> From 45181e5c65efc7c33ccc6889cefe4a885a381a6b Mon Sep 17 00:00:00 2001 From: dan-searle Date: Fri, 1 Feb 2019 09:45:06 +0000 Subject: [PATCH 53/71] Extract arrayToSentence function to be a component. --- .../x-topic-search/src/ResultContainer.jsx | 17 ++--------------- .../src/SuggestionsAsSentence.jsx | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 15 deletions(-) create mode 100644 components/x-topic-search/src/SuggestionsAsSentence.jsx diff --git a/components/x-topic-search/src/ResultContainer.jsx b/components/x-topic-search/src/ResultContainer.jsx index cc6906168..75ab3dc22 100644 --- a/components/x-topic-search/src/ResultContainer.jsx +++ b/components/x-topic-search/src/ResultContainer.jsx @@ -4,20 +4,7 @@ import classNames from 'classnames'; import SuggestionList from './SuggestionList'; import NoSuggestions from './NoSuggestions'; - -// transform like this => topic1, topic2 and topic3 -const arrayToSentence = followedSuggestions => { - const topicsLength = followedSuggestions.length; - - return followedSuggestions.map((topic, index) => ( - - {topicsLength > 1 && index === topicsLength - 1 && ' and '} - {topic.prefLabel} - {index < topicsLength - 2 && ', '} - - )); -}; - +import SuggestionsAsSentence from './SuggestionsAsSentence'; export default ({ followedSuggestions, searchTerm, csrfToken, followedTopicIds, unfollowedSuggestions }) => { const hasFollowedSuggestions = followedSuggestions.length > 0; @@ -35,7 +22,7 @@ export default ({ followedSuggestions, searchTerm, csrfToken, followedTopicIds, {!hasUnfollowedSuggestions && hasFollowedSuggestions &&
      - You already follow { arrayToSentence(followedSuggestions) } + You already follow
      } {!hasUnfollowedSuggestions && !hasFollowedSuggestions && diff --git a/components/x-topic-search/src/SuggestionsAsSentence.jsx b/components/x-topic-search/src/SuggestionsAsSentence.jsx new file mode 100644 index 000000000..c7c3f3d8f --- /dev/null +++ b/components/x-topic-search/src/SuggestionsAsSentence.jsx @@ -0,0 +1,17 @@ +import { h } from '@financial-times/x-engine'; + +export default ({ suggestions }) => { + const suggestionsLength = suggestions.length; + + return ( + + {suggestions.map((suggestion, index) => ( + + {suggestionsLength > 1 && index === suggestionsLength - 1 && ' and '} + {suggestion.prefLabel} + {index < suggestionsLength - 2 && ', '} + ) + )} + + ); +}; From 85d16af2a436b179324dd7363f5ae875688ac0db Mon Sep 17 00:00:00 2001 From: dan-searle Date: Fri, 1 Feb 2019 10:56:21 +0000 Subject: [PATCH 54/71] Don't show any search results if the searchTerm length is less than the minSearchLength. --- components/x-topic-search/src/TopicSearch.jsx | 9 +++------ components/x-topic-search/src/lib/get-suggestions.js | 1 - 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/components/x-topic-search/src/TopicSearch.jsx b/components/x-topic-search/src/TopicSearch.jsx index 6cc5ef522..d9a83248d 100644 --- a/components/x-topic-search/src/TopicSearch.jsx +++ b/components/x-topic-search/src/TopicSearch.jsx @@ -46,9 +46,8 @@ class TopicSearch extends Component { if (searchTerm.length >= this.minSearchLength) { this.getSuggestions(searchTerm, this.maxSuggestions, this.apiUrl, this.props.followedTopicIds) - .then(({ resultsForTerm, followedSuggestions, unfollowedSuggestions }) => { + .then(({ followedSuggestions, unfollowedSuggestions }) => { this.setState({ - resultsForTerm, followedSuggestions, unfollowedSuggestions, showResult: true @@ -56,13 +55,11 @@ class TopicSearch extends Component { }) .catch(() => { this.setState({ - resultsForTerm: null, showResult: false }); }); } else { this.setState({ - resultsForTerm: null, showResult: false }); } @@ -86,7 +83,7 @@ class TopicSearch extends Component { render() { const { csrfToken, followedTopicIds } = this.props; - const { followedSuggestions, resultsForTerm, searchTerm, showResult, unfollowedSuggestions } = this.state; + const { followedSuggestions, searchTerm, showResult, unfollowedSuggestions } = this.state; return (
      this.rootEl = el}> @@ -110,7 +107,7 @@ class TopicSearch extends Component { />
      - { showResult && resultsForTerm === searchTerm && + { showResult && searchTerm.length >= this.minSearchLength && { return response.json(); }) .then(suggestions => ({ - resultsForTerm: searchTerm, followedSuggestions: suggestions.filter(suggestion => followedTopicIds.includes(suggestion.id)), unfollowedSuggestions: suggestions.filter(suggestion => !followedTopicIds.includes(suggestion.id)) })); From 9adec5062bda3772b8079d771b82ebdbda2947c5 Mon Sep 17 00:00:00 2001 From: dan-searle Date: Fri, 1 Feb 2019 11:41:32 +0000 Subject: [PATCH 55/71] Remove "you already follow..." messaging and just display any suggestions with their follow buttons in the appropriate states. --- .../__tests__/x-topic-search.test.jsx | 61 +++++-------------- .../x-topic-search/src/ResultContainer.jsx | 32 ---------- .../src/SuggestionsAsSentence.jsx | 17 ------ components/x-topic-search/src/TopicSearch.jsx | 36 +++++------ .../x-topic-search/src/TopicSearch.scss | 8 --- .../x-topic-search/src/lib/get-suggestions.js | 7 +-- 6 files changed, 36 insertions(+), 125 deletions(-) delete mode 100644 components/x-topic-search/src/ResultContainer.jsx delete mode 100644 components/x-topic-search/src/SuggestionsAsSentence.jsx diff --git a/components/x-topic-search/__tests__/x-topic-search.test.jsx b/components/x-topic-search/__tests__/x-topic-search.test.jsx index 5bc356fbc..04fe1b4da 100644 --- a/components/x-topic-search/__tests__/x-topic-search.test.jsx +++ b/components/x-topic-search/__tests__/x-topic-search.test.jsx @@ -3,17 +3,12 @@ const { h } = require('@financial-times/x-engine'); const { mount } = require('@financial-times/x-test-utils/enzyme'); const { TopicSearch } = require('../'); -const searchTerm = 'Dog'; -const searchTermNoResult = 'Blobfish'; -const searchTermAllFollowed = 'Cat'; const minSearchLength = 3; const maxSuggestions = 3; const apiUrl = 'api-url'; -const alreadyFollowedTopics = [ - { uuid: 'Cat-House-id', name: 'Cat House' }, - { uuid: 'Cat-Food-id', name: 'Cat Food' }, - { uuid: 'Cat-Toys-id', name: 'Cat Toys' } -]; +const FOLLOWED_TOPIC_ID1 = 'Cat-House-id'; +const FOLLOWED_TOPIC_ID2 = 'Cat-Food-id'; +const UNFOLLOWED_TOPIC_ID1 = 'Cat-Toys-id'; describe('x-topic-search', () => { const buildSearchUrl = term => `${apiUrl}?count=${maxSuggestions}&partial=${term}`; @@ -29,7 +24,7 @@ describe('x-topic-search', () => { minSearchLength, maxSuggestions, apiUrl, - followedTopicIds: alreadyFollowedTopics.map(topic => topic.uuid), + followedTopicIds: [FOLLOWED_TOPIC_ID1, FOLLOWED_TOPIC_ID2], }; target = mount(); }); @@ -64,17 +59,17 @@ describe('x-topic-search', () => { }); describe('given searchTerm which has some topic suggestions to follow', () => { - const apiUrlWithResults = buildSearchUrl(searchTerm); - const unfollowedTopicSuggestions = [ - { id: 'Dog-House-id', prefLabel: 'Dog House', url: 'Dog-House-url' }, - { id: 'Dog-Food-id', prefLabel: 'Dog Food', url: 'Dog-Food-url' }, - { id: 'Dog-Toys-id', prefLabel: 'Dog Toys', url: 'Dog-Toys-url' } + const apiUrlWithResults = buildSearchUrl('Cat'); + const results = [ + { id: FOLLOWED_TOPIC_ID1, prefLabel: 'Cat House', url: 'Cat-House-url' }, + { id: FOLLOWED_TOPIC_ID2, prefLabel: 'Cat Food', url: 'Cat-Food-url' }, + { id: UNFOLLOWED_TOPIC_ID1, prefLabel: 'Cat Toys', url: 'Cat-Toys-url' } ]; - fetchMock.get(apiUrlWithResults, unfollowedTopicSuggestions); + fetchMock.get(apiUrlWithResults, results); beforeEach(() => { - return enterSearchTerm(searchTerm); + return enterSearchTerm('Cat'); }); it('requests the topic suggestions with count set to maxSuggestions', () => { @@ -89,23 +84,23 @@ describe('x-topic-search', () => { it('renders links and follow buttons for each suggestion', () => { const suggestionsList = target.render().find('li'); - unfollowedTopicSuggestions.forEach((topic, index) => { + results.forEach((topic, index) => { const suggestion = suggestionsList.eq(index); expect(suggestion.find('a').text()).toEqual(topic.prefLabel); expect(suggestion.find('a').attr('href')).toEqual(topic.url); - expect(suggestion.find('button')).toHaveLength(1); + expect(suggestion.find('button').text()).toEqual(topic.id === UNFOLLOWED_TOPIC_ID1 ? 'Add to myFT' : 'Added'); }); }) }); describe('given searchTerm which has no topic suggestions to follow', () => { - const apiUrlNoResults = buildSearchUrl(searchTermNoResult); + const apiUrlNoResults = buildSearchUrl('Dog'); fetchMock.get(apiUrlNoResults, []); beforeEach(() => { - return enterSearchTerm(searchTermNoResult); + return enterSearchTerm('Dog'); }); it('requests from the api and renders the no matching topics message', () => { @@ -117,30 +112,4 @@ describe('x-topic-search', () => { expect(resultContainer.find('h2').text()).toMatch('No topics matching'); }); }); - - describe('given searchTerm which results in all suggestions already followed', () => { - const apiUrlAllFollowed = buildSearchUrl(searchTermAllFollowed); - - fetchMock.get(apiUrlAllFollowed, alreadyFollowedTopics.map(topic => ({ - id: topic.uuid, - prefLabel: topic.name, - url: topic.name.replace(' ', '-') - }))); - - beforeEach(() => { - return enterSearchTerm(searchTermAllFollowed); - }); - - it('requests the suggestions from the api', () => { - expect(fetchMock.called(apiUrlAllFollowed)).toBe(true); - }); - - it('renders the "already followed" message with names of the topics', () => { - const resultContainer = target.render().children('div').eq(1); - - expect(resultContainer).toHaveLength(1); - expect(resultContainer.text()) - .toMatch(`You already follow ${alreadyFollowedTopics[0].name}, ${alreadyFollowedTopics[1].name} and ${alreadyFollowedTopics[2].name}`); - }); - }); }); diff --git a/components/x-topic-search/src/ResultContainer.jsx b/components/x-topic-search/src/ResultContainer.jsx deleted file mode 100644 index 75ab3dc22..000000000 --- a/components/x-topic-search/src/ResultContainer.jsx +++ /dev/null @@ -1,32 +0,0 @@ -import { h } from '@financial-times/x-engine'; -import styles from './TopicSearch.scss'; -import classNames from 'classnames'; - -import SuggestionList from './SuggestionList'; -import NoSuggestions from './NoSuggestions'; -import SuggestionsAsSentence from './SuggestionsAsSentence'; - -export default ({ followedSuggestions, searchTerm, csrfToken, followedTopicIds, unfollowedSuggestions }) => { - const hasFollowedSuggestions = followedSuggestions.length > 0; - const hasUnfollowedSuggestions = unfollowedSuggestions.length > 0; - - return ( -
      - {hasUnfollowedSuggestions && - } - - {!hasUnfollowedSuggestions && hasFollowedSuggestions && -
      - You already follow -
      } - - {!hasUnfollowedSuggestions && !hasFollowedSuggestions && - } -
      - ); -}; diff --git a/components/x-topic-search/src/SuggestionsAsSentence.jsx b/components/x-topic-search/src/SuggestionsAsSentence.jsx deleted file mode 100644 index c7c3f3d8f..000000000 --- a/components/x-topic-search/src/SuggestionsAsSentence.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import { h } from '@financial-times/x-engine'; - -export default ({ suggestions }) => { - const suggestionsLength = suggestions.length; - - return ( - - {suggestions.map((suggestion, index) => ( - - {suggestionsLength > 1 && index === suggestionsLength - 1 && ' and '} - {suggestion.prefLabel} - {index < suggestionsLength - 2 && ', '} - ) - )} - - ); -}; diff --git a/components/x-topic-search/src/TopicSearch.jsx b/components/x-topic-search/src/TopicSearch.jsx index d9a83248d..0675f849d 100644 --- a/components/x-topic-search/src/TopicSearch.jsx +++ b/components/x-topic-search/src/TopicSearch.jsx @@ -3,8 +3,8 @@ import styles from './TopicSearch.scss'; import classNames from 'classnames'; import getSuggestions from './lib/get-suggestions.js'; import debounce from 'debounce-promise'; - -import ResultContainer from './ResultContainer'; +import SuggestionList from './SuggestionList'; +import NoSuggestions from './NoSuggestions'; class TopicSearch extends Component { constructor(props) { @@ -20,10 +20,9 @@ class TopicSearch extends Component { this.handleInteractionOutside = this.handleInteractionOutside.bind(this); this.state = { + followedTopicIds: props.followedTopicIds || [], searchTerm: '', - showResult: false, - followedSuggestions: [], - unfollowedSuggestions: [] + showResult: false }; } @@ -45,11 +44,10 @@ class TopicSearch extends Component { this.setState({ searchTerm }); if (searchTerm.length >= this.minSearchLength) { - this.getSuggestions(searchTerm, this.maxSuggestions, this.apiUrl, this.props.followedTopicIds) - .then(({ followedSuggestions, unfollowedSuggestions }) => { + this.getSuggestions(searchTerm, this.maxSuggestions, this.apiUrl) + .then(({ suggestions }) => { this.setState({ - followedSuggestions, - unfollowedSuggestions, + suggestions, showResult: true }); }) @@ -83,7 +81,7 @@ class TopicSearch extends Component { render() { const { csrfToken, followedTopicIds } = this.props; - const { followedSuggestions, searchTerm, showResult, unfollowedSuggestions } = this.state; + const { searchTerm, showResult, suggestions } = this.state; return (
      this.rootEl = el}> @@ -107,13 +105,17 @@ class TopicSearch extends Component { />
      - { showResult && searchTerm.length >= this.minSearchLength && - } + {showResult && searchTerm.length >= this.minSearchLength && +
      + {suggestions.length > 0 ? + : + } +
      }
      ); } diff --git a/components/x-topic-search/src/TopicSearch.scss b/components/x-topic-search/src/TopicSearch.scss index bca94f69d..759982612 100644 --- a/components/x-topic-search/src/TopicSearch.scss +++ b/components/x-topic-search/src/TopicSearch.scss @@ -117,11 +117,3 @@ margin-bottom: 0; list-style-type: disc; } - -.all-followed { - @include oTypographySans($scale: 1); - color: oColorsGetPaletteColor('black-70'); - text-align: left; - padding: 0; - margin: 0; -} diff --git a/components/x-topic-search/src/lib/get-suggestions.js b/components/x-topic-search/src/lib/get-suggestions.js index ce2caef4b..d7c85db08 100644 --- a/components/x-topic-search/src/lib/get-suggestions.js +++ b/components/x-topic-search/src/lib/get-suggestions.js @@ -4,7 +4,7 @@ const addQueryParamToUrl = (name, value, url, append = true) => { return append === true ? `${url}&${queryParam}` : `${url}?${queryParam}`; }; -export default (searchTerm, maxSuggestions, apiUrl, followedTopicIds) => { +export default (searchTerm, maxSuggestions, apiUrl) => { const dataSrc = addQueryParamToUrl('count', maxSuggestions, apiUrl, false); const url = addQueryParamToUrl('partial', searchTerm.replace(' ', '+'), dataSrc); @@ -16,8 +16,5 @@ export default (searchTerm, maxSuggestions, apiUrl, followedTopicIds) => { return response.json(); }) - .then(suggestions => ({ - followedSuggestions: suggestions.filter(suggestion => followedTopicIds.includes(suggestion.id)), - unfollowedSuggestions: suggestions.filter(suggestion => !followedTopicIds.includes(suggestion.id)) - })); + .then(suggestions => ({ suggestions })); }; From 1c3edc0ce6bdf34b512ff5432f7b37006ef5b14d Mon Sep 17 00:00:00 2001 From: fenglish Date: Fri, 1 Feb 2019 10:40:17 +0000 Subject: [PATCH 56/71] Overwrite top/bottom padding 0 to keep input height the same across browsers It seems Safari includes the height of search cancel button as the input height and Chrome doesn't. Overwrite padding top/bottom 0 and keep the input height the min-height(40px) no matter whether the browser includes the cancel button height as the input height or not. --- components/x-topic-search/src/TopicSearch.scss | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/components/x-topic-search/src/TopicSearch.scss b/components/x-topic-search/src/TopicSearch.scss index 759982612..d558827e9 100644 --- a/components/x-topic-search/src/TopicSearch.scss +++ b/components/x-topic-search/src/TopicSearch.scss @@ -34,7 +34,7 @@ margin: 0; border: none; border-bottom: 2px solid oColorsGetPaletteColor('white'); - padding-left: 24px; + padding: 0 9px 0 24px; max-width: none; color: oColorsGetPaletteColor('white'); background: transparent; @@ -47,6 +47,12 @@ @include oIconsGetIcon('cross', oColorsGetPaletteColor('white'), 26); -webkit-appearance: none; } + + &::-webkit-search-decoration, + &::-webkit-search-results-button, + &::-webkit-search-results-decoration { + display: none; + } } .result-container { From cc8ddb42f6a78144efbc5410b15047b26b35954e Mon Sep 17 00:00:00 2001 From: fenglish Date: Mon, 4 Feb 2019 14:59:40 +0000 Subject: [PATCH 57/71] Remove o-forms The design doesn't have much commonality with o-forms and many of the styles are being overwritten. --- components/x-topic-search/bower.json | 1 - components/x-topic-search/src/TopicSearch.scss | 7 +++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/components/x-topic-search/bower.json b/components/x-topic-search/bower.json index 1837995b5..f92919018 100644 --- a/components/x-topic-search/bower.json +++ b/components/x-topic-search/bower.json @@ -4,7 +4,6 @@ "private": true, "dependencies": { "o-icons": "^5.8.0", - "o-forms": "^5.9.0", "o-typography": "^5.7.8", "o-colors": "^4.7.7" } diff --git a/components/x-topic-search/src/TopicSearch.scss b/components/x-topic-search/src/TopicSearch.scss index d558827e9..ac07c4b51 100644 --- a/components/x-topic-search/src/TopicSearch.scss +++ b/components/x-topic-search/src/TopicSearch.scss @@ -1,15 +1,12 @@ @import 'o-icons/main'; @import 'o-colors/main'; @import 'o-typography/main'; -@import 'o-forms/main'; :global { @import "~@financial-times/x-follow-button/dist/FollowButton"; } .container { - @include oFormsBaseFeatures; - @include oFormsWideFeature; position: relative; text-align: center; background-color: oColorsGetPaletteColor('claret-70'); @@ -29,8 +26,10 @@ } .input { - @include oFormsCommonFieldBase; @include oTypographySans($scale: 0); + -webkit-appearance: none; + width: 100%; + min-height: 40px; margin: 0; border: none; border-bottom: 2px solid oColorsGetPaletteColor('white'); From 396d0dbdf2e8ff27178d5c5dbb261dddae3bfeeb Mon Sep 17 00:00:00 2001 From: fenglish Date: Tue, 5 Feb 2019 14:04:52 +0000 Subject: [PATCH 58/71] Select the text when input box is changed from blur to focus --- .../__tests__/x-topic-search.test.jsx | 12 ++++++++++++ components/x-topic-search/src/TopicSearch.jsx | 14 ++++++++++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/components/x-topic-search/__tests__/x-topic-search.test.jsx b/components/x-topic-search/__tests__/x-topic-search.test.jsx index 04fe1b4da..1bbf87aea 100644 --- a/components/x-topic-search/__tests__/x-topic-search.test.jsx +++ b/components/x-topic-search/__tests__/x-topic-search.test.jsx @@ -43,6 +43,18 @@ describe('x-topic-search', () => { }); }); + describe('when input receives focus', () => { + it('selects the text in the input', () => { + const selectMock = jest.fn(); + const inputBox = target.find('input'); + + inputBox.simulate('blur'); + inputBox.simulate('focus', { target: { select: selectMock }}); + + expect(selectMock).toHaveBeenCalledTimes(1); + }); + }); + describe('given inputted text is shorter than minSearchLength', () => { const apiUrlWithResults = buildSearchUrl('a'); diff --git a/components/x-topic-search/src/TopicSearch.jsx b/components/x-topic-search/src/TopicSearch.jsx index 0675f849d..4cbdcc8bf 100644 --- a/components/x-topic-search/src/TopicSearch.jsx +++ b/components/x-topic-search/src/TopicSearch.jsx @@ -16,7 +16,8 @@ class TopicSearch extends Component { this.getSuggestions = debounce(getSuggestions, 150); this.outsideEvents = ['focusout', 'focusin', 'click']; this.handleInputChange = this.handleInputChange.bind(this); - this.handleInputClickOrFocus = this.handleInputClickOrFocus.bind(this); + this.handleInputClick = this.handleInputClick.bind(this); + this.handleInputFocus = this.handleInputFocus.bind(this); this.handleInteractionOutside = this.handleInteractionOutside.bind(this); this.state = { @@ -71,7 +72,7 @@ class TopicSearch extends Component { } } - handleInputClickOrFocus() { + handleInputClick() { if (this.state.searchTerm.length >= this.minSearchLength) { this.setState({ showResult: true @@ -79,6 +80,11 @@ class TopicSearch extends Component { } } + handleInputFocus(event) { + event.target.select(); + this.handleInputClick(); + } + render() { const { csrfToken, followedTopicIds } = this.props; const { searchTerm, showResult, suggestions } = this.state; @@ -100,8 +106,8 @@ class TopicSearch extends Component { data-trackable="topic-search" autoComplete="off" onInput={this.handleInputChange} - onClick={this.handleInputClickOrFocus} - onFocus={this.handleInputClickOrFocus} + onClick={this.handleInputClick} + onFocus={this.handleInputFocus} />
      From 55f0a053dd677c8be7ab166bfaee30f91252ec78 Mon Sep 17 00:00:00 2001 From: fenglish Date: Wed, 6 Feb 2019 11:10:31 +0000 Subject: [PATCH 59/71] Delete outdated explanation --- components/x-topic-search/readme.md | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/components/x-topic-search/readme.md b/components/x-topic-search/readme.md index 3983fc929..35bac49e7 100644 --- a/components/x-topic-search/readme.md +++ b/components/x-topic-search/readme.md @@ -34,29 +34,6 @@ All `x-` components are designed to be compatible with a variety of runtimes, no [jsx-wtf]: https://jasonformat.com/wtf-is-jsx/ -### Hide Result - -Your x-topic-search could hide the result container, which displays search result or messages, by external triggers. - -[x-interaction triggering-actions-externally](https://github.com/Financial-Times/x-dash/tree/master/components/x-interaction#triggering-actions-externally) - -``` -const container = ... -let topicSearchActions; - -['focusout', 'focusin', 'click'].forEach(action => { - document.body.addEventListener(action, event => { - if(!container.contains(event.target)) { - topicSearchActions.hideResult(); - } - }); -}); - -render topicSearchActions = actions} - /> -``` ### Properties From 2a4a01d1e3f5feb6c187ff2c3d44ba6bcc2a7527 Mon Sep 17 00:00:00 2001 From: fenglish Date: Wed, 6 Feb 2019 11:33:03 +0000 Subject: [PATCH 60/71] Delete spaces --- .../x-topic-search/src/NoSuggestions.jsx | 8 +++--- .../x-topic-search/src/SuggestionList.jsx | 26 +++++++++---------- components/x-topic-search/src/TopicSearch.jsx | 10 +++---- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/components/x-topic-search/src/NoSuggestions.jsx b/components/x-topic-search/src/NoSuggestions.jsx index 444c1adb6..b3b50b905 100644 --- a/components/x-topic-search/src/NoSuggestions.jsx +++ b/components/x-topic-search/src/NoSuggestions.jsx @@ -3,15 +3,15 @@ import styles from './TopicSearch.scss'; import classNames from 'classnames'; export default ({ searchTerm }) => ( -
      +
      -

      - No topics matching { searchTerm } +

      + No topics matching {searchTerm}

      Suggestions:

      -
        +
        • Make sure that all words are spelled correctly.
        • Try different keywords.
        • Try more general keywords.
        • diff --git a/components/x-topic-search/src/SuggestionList.jsx b/components/x-topic-search/src/SuggestionList.jsx index d056b34e2..6b38d1b0f 100644 --- a/components/x-topic-search/src/SuggestionList.jsx +++ b/components/x-topic-search/src/SuggestionList.jsx @@ -4,26 +4,26 @@ import styles from './TopicSearch.scss'; import classNames from 'classnames'; export default ({ suggestions, searchTerm, csrfToken, followedTopicIds = [] }) => ( -
            +
              - { suggestions.map((suggestion, index) => ( -
            • ( +
            • + data-concept-id={suggestion.id} + data-trackable-meta={'{"search-term":"' + searchTerm + '"}'}> - { suggestion.prefLabel } + className={classNames(styles["suggestion__name"])} + href={suggestion.url || `/stream/${suggestion.id}`}> + {suggestion.prefLabel} + conceptId={suggestion.id} + conceptName={suggestion.prefLabel} + csrfToken={csrfToken} + isFollowed={followedTopicIds.includes(suggestion.id)}/>
            • ))} diff --git a/components/x-topic-search/src/TopicSearch.jsx b/components/x-topic-search/src/TopicSearch.jsx index 4cbdcc8bf..16a189299 100644 --- a/components/x-topic-search/src/TopicSearch.jsx +++ b/components/x-topic-search/src/TopicSearch.jsx @@ -90,19 +90,19 @@ class TopicSearch extends Component { const { searchTerm, showResult, suggestions } = this.state; return ( -
              this.rootEl = el}> +
              this.rootEl = el}>

              Search for topics, authors, companies, or other areas of interest

              -
              - +
              + : - } + }
              }
              ); From 93d1606c455e94489c8d37555ca7aa1349ffd161 Mon Sep 17 00:00:00 2001 From: fenglish Date: Wed, 6 Feb 2019 11:34:57 +0000 Subject: [PATCH 61/71] Set more specific key instead of map index --- components/x-topic-search/src/SuggestionList.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/x-topic-search/src/SuggestionList.jsx b/components/x-topic-search/src/SuggestionList.jsx index 6b38d1b0f..6d31ef262 100644 --- a/components/x-topic-search/src/SuggestionList.jsx +++ b/components/x-topic-search/src/SuggestionList.jsx @@ -6,9 +6,9 @@ import classNames from 'classnames'; export default ({ suggestions, searchTerm, csrfToken, followedTopicIds = [] }) => (
                - {suggestions.map((suggestion, index) => ( + {suggestions.map(suggestion => (
              • From 0ba98794be94d33e42d49d21a96b7fad63a85464 Mon Sep 17 00:00:00 2001 From: fenglish Date: Wed, 6 Feb 2019 14:27:46 +0000 Subject: [PATCH 62/71] Update README --- components/x-topic-search/readme.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/components/x-topic-search/readme.md b/components/x-topic-search/readme.md index 35bac49e7..ddd1460c0 100644 --- a/components/x-topic-search/readme.md +++ b/components/x-topic-search/readme.md @@ -1,6 +1,10 @@ # x-topic-search -This module provides a topic search bar. +This module allows a user to search for topics by name, and follow them. If an already-followed topic is returned in the search results, then those topics are indicated as such. + +The search results are fetched from the api whose url is passed as a property. +[next-myft-page](https://github.com/Financial-Times/next-myft-page/blob/master/client/components/topic-search/TopicSearchContainer.jsx#L9) +uses [next-tag-facets-api](https://github.com/Financial-Times/next-tag-facets-api). ## Installation @@ -35,6 +39,9 @@ All `x-` components are designed to be compatible with a variety of runtimes, no [jsx-wtf]: https://jasonformat.com/wtf-is-jsx/ +The consumer of this component needs to update `followedTopicIds` every time when users follow or unfollow topics. + + ### Properties Property | Type | Required | Note From 9a1297ad4138ee2611d72a45e5e79e6a0a52d509 Mon Sep 17 00:00:00 2001 From: dan-searle Date: Fri, 8 Feb 2019 11:43:08 +0000 Subject: [PATCH 63/71] Correct left alignment of no-suggestions bullet list. --- components/x-topic-search/src/TopicSearch.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/components/x-topic-search/src/TopicSearch.scss b/components/x-topic-search/src/TopicSearch.scss index ac07c4b51..8e3bfd5a0 100644 --- a/components/x-topic-search/src/TopicSearch.scss +++ b/components/x-topic-search/src/TopicSearch.scss @@ -120,5 +120,6 @@ .no-suggestions__message { margin-top: 12px; margin-bottom: 0; + padding-left: 20px; list-style-type: disc; } From 4f40d0169d5ad82fa45f814f7b44f1d9d1a0c3b9 Mon Sep 17 00:00:00 2001 From: Rowan Beentje Date: Thu, 7 Mar 2019 09:50:45 +0000 Subject: [PATCH 64/71] Unlock x-follow-button version to include compatible versions --- components/x-topic-search/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/x-topic-search/package.json b/components/x-topic-search/package.json index 22c52604e..83f1f02b8 100644 --- a/components/x-topic-search/package.json +++ b/components/x-topic-search/package.json @@ -18,7 +18,7 @@ "license": "ISC", "dependencies": { "@financial-times/x-engine": "file:../../packages/x-engine", - "@financial-times/x-follow-button": "0.0.11", + "@financial-times/x-follow-button": "^0.0.11", "classnames": "^2.2.6", "debounce-promise": "^3.1.0" }, From 13dcebcc23ad51ca3553c84b8506baf6ae50c691 Mon Sep 17 00:00:00 2001 From: Rowan Beentje Date: Thu, 7 Mar 2019 11:04:01 +0000 Subject: [PATCH 65/71] Bump x-follow-button dependency of x-topic-search to ^0.0.12 --- components/x-topic-search/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/x-topic-search/package.json b/components/x-topic-search/package.json index 83f1f02b8..df5555b31 100644 --- a/components/x-topic-search/package.json +++ b/components/x-topic-search/package.json @@ -18,7 +18,7 @@ "license": "ISC", "dependencies": { "@financial-times/x-engine": "file:../../packages/x-engine", - "@financial-times/x-follow-button": "^0.0.11", + "@financial-times/x-follow-button": "^0.0.12", "classnames": "^2.2.6", "debounce-promise": "^3.1.0" }, From b0149e159dc3a357fb91696a7d841b8c73550b55 Mon Sep 17 00:00:00 2001 From: Pavel Pichrt Date: Mon, 11 Mar 2019 15:51:55 +0000 Subject: [PATCH 66/71] AT-2015: Use a render prop for the follow button to make it work with x-follow-button 0.0.12 --- .../x-topic-search/src/SuggestionList.jsx | 52 +++++++++++-------- components/x-topic-search/src/TopicSearch.jsx | 3 +- 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/components/x-topic-search/src/SuggestionList.jsx b/components/x-topic-search/src/SuggestionList.jsx index 6d31ef262..45a2e3e72 100644 --- a/components/x-topic-search/src/SuggestionList.jsx +++ b/components/x-topic-search/src/SuggestionList.jsx @@ -3,30 +3,38 @@ import { FollowButton } from '@financial-times/x-follow-button'; import styles from './TopicSearch.scss'; import classNames from 'classnames'; -export default ({ suggestions, searchTerm, csrfToken, followedTopicIds = [] }) => ( -
                  +const defaultFollowButtonRender = (concept, csrfToken, followedTopicIds) => ( + +); - {suggestions.map(suggestion => ( -
                • +export default ({ suggestions, renderFollowButton, searchTerm, csrfToken, followedTopicIds = [] }) => { + renderFollowButton = typeof renderFollowButton === 'function' ? renderFollowButton : defaultFollowButtonRender; - - {suggestion.prefLabel} - + return ( + -); + {renderFollowButton(suggestion, csrfToken, followedTopicIds)} +
                • + ))} + +
                + ); +}; diff --git a/components/x-topic-search/src/TopicSearch.jsx b/components/x-topic-search/src/TopicSearch.jsx index 16a189299..9fd32b871 100644 --- a/components/x-topic-search/src/TopicSearch.jsx +++ b/components/x-topic-search/src/TopicSearch.jsx @@ -86,7 +86,7 @@ class TopicSearch extends Component { } render() { - const { csrfToken, followedTopicIds } = this.props; + const { csrfToken, followedTopicIds, renderFollowButton } = this.props; const { searchTerm, showResult, suggestions } = this.state; return ( @@ -119,6 +119,7 @@ class TopicSearch extends Component { followedTopicIds={followedTopicIds} searchTerm={searchTerm} suggestions={suggestions} + renderFollowButton={renderFollowButton} /> : }
              } From a0f64a5f939e4bc8486d8d4c9bacbb3b72c77c87 Mon Sep 17 00:00:00 2001 From: Pavel Pichrt Date: Tue, 12 Mar 2019 14:39:00 +0000 Subject: [PATCH 67/71] AT-2015: Added prop docs --- components/x-topic-search/readme.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/components/x-topic-search/readme.md b/components/x-topic-search/readme.md index ddd1460c0..73e0f082f 100644 --- a/components/x-topic-search/readme.md +++ b/components/x-topic-search/readme.md @@ -2,7 +2,7 @@ This module allows a user to search for topics by name, and follow them. If an already-followed topic is returned in the search results, then those topics are indicated as such. -The search results are fetched from the api whose url is passed as a property. +The search results are fetched from the api whose url is passed as a property. [next-myft-page](https://github.com/Financial-Times/next-myft-page/blob/master/client/components/topic-search/TopicSearchContainer.jsx#L9) uses [next-tag-facets-api](https://github.com/Financial-Times/next-tag-facets-api). @@ -44,10 +44,11 @@ The consumer of this component needs to update `followedTopicIds` every time whe ### Properties -Property | Type | Required | Note -------------------|--------|----------|------------------ -`minSearchLength` | Number | No | Minimum chars to start search. Default is 2 -`maxSuggestions` | Number | No | Maximum number to display suggestions. Default is 5 -`apiUrl` | String | Yes | The url to use when making requests to get topics -`followedTopicIds`| Array | Yes | Array of followed topic `id`s. -`csrfToken` | String | Yes | Value included in a hidden form field for x-follow-button +Property | Type | Required | Note +---------------------|----------|----------|------------------ +`minSearchLength` | Number | No | Minimum chars to start search. Default is 2 +`maxSuggestions` | Number | No | Maximum number to display suggestions. Default is 5 +`apiUrl` | String | Yes | The url to use when making requests to get topics +`followedTopicIds` | Array | Yes | Array of followed topic `id`s. +`csrfToken` | String | Yes | Value included in a hidden form field for x-follow-button +`renderFollowButton` | Function | No | Optional render prop for the follow button From 0d6ba096c0e12083f8a2ffd2c457ec30a376d27f Mon Sep 17 00:00:00 2001 From: rowanbeentje Date: Wed, 24 Nov 2021 09:13:10 +0000 Subject: [PATCH 68/71] Miggrate x-topic-search Storybook setup to v6 format --- components/x-topic-search/stories/index.js | 20 ------- components/x-topic-search/stories/knobs.js | 19 ------- .../x-topic-search/stories/topic-search.js | 19 ------- components/x-topic-search/storybook/index.jsx | 52 +++++++++++++++++++ 4 files changed, 52 insertions(+), 58 deletions(-) delete mode 100644 components/x-topic-search/stories/index.js delete mode 100644 components/x-topic-search/stories/knobs.js delete mode 100644 components/x-topic-search/stories/topic-search.js create mode 100644 components/x-topic-search/storybook/index.jsx diff --git a/components/x-topic-search/stories/index.js b/components/x-topic-search/stories/index.js deleted file mode 100644 index 601676cb3..000000000 --- a/components/x-topic-search/stories/index.js +++ /dev/null @@ -1,20 +0,0 @@ -const { TopicSearch } = require('../'); - -exports.component = TopicSearch; - -exports.package = require('../package.json'); - -// Set up basic document styling using the Origami build service -exports.dependencies = { - 'o-normalise': '^1.6.0', - 'o-typography': '^5.5.0', - 'o-colors': "^4.7.7", - 'o-icons': "^5.8.0", -}; - -exports.stories = [ - require('./topic-search') -]; - -exports.knobs = require('./knobs'); - diff --git a/components/x-topic-search/stories/knobs.js b/components/x-topic-search/stories/knobs.js deleted file mode 100644 index 7809db53e..000000000 --- a/components/x-topic-search/stories/knobs.js +++ /dev/null @@ -1,19 +0,0 @@ -module.exports = (data, { select }) => { - return { - followedTopicIds() { - return select( - 'Followed Topics', - { - None: [], - 'World Elephant Water Polo': ['f95d1e16-2307-4feb-b3ff-6f224798aa49'], - 'Brexit, Brexit Briefing, Brexit Unspun Podcast': [ - '19b95057-4614-45fb-9306-4d54049354db', - '464cc2f2-395e-4c36-bb29-01727fc95558', - 'c4e899ed-157e-4446-86f0-5a65803dc07a' - ] - }, - [] - ); - } - }; -}; diff --git a/components/x-topic-search/stories/topic-search.js b/components/x-topic-search/stories/topic-search.js deleted file mode 100644 index 387b4f604..000000000 --- a/components/x-topic-search/stories/topic-search.js +++ /dev/null @@ -1,19 +0,0 @@ -exports.title = 'Topic Search Bar'; - -const data = { - minSearchLength: 2, - maxSuggestions: 10, - apiUrl: '//tag-facets-api.ft.com/annotations', - followedTopicIds: [ - 'f95d1e16-2307-4feb-b3ff-6f224798aa49' - ], - csrfToken: 'csrfToken' -}; - -exports.data = data; - -exports.knobs = Object.keys(data); - -// This reference is only required for hot module loading in development -// -exports.m = module; diff --git a/components/x-topic-search/storybook/index.jsx b/components/x-topic-search/storybook/index.jsx new file mode 100644 index 000000000..3cbc070f9 --- /dev/null +++ b/components/x-topic-search/storybook/index.jsx @@ -0,0 +1,52 @@ +import React from 'react' +import { TopicSearch } from '../src/TopicSearch' +import BuildService from '../../../.storybook/build-service' + +// Set up basic document styling using the Origami build service +const dependencies = { + 'o-normalise': '^2.0.0', + 'o-typography': '^6.0.0', + 'o-colors': '^5.0.0', + 'o-icons': '^6.0.0' +} + +export default { + title: 'x-topic-search' +} + +export const _TopicSearchBar = (args) => { + return ( +
              + + +
              + ) +} + +_TopicSearchBar.args = { + minSearchLength: 2, + maxSuggestions: 10, + apiUrl: '//tag-facets-api.ft.com/annotations', + followedTopicIds: ['f95d1e16-2307-4feb-b3ff-6f224798aa49'], + csrfToken: 'csrfToken' +} + +_TopicSearchBar.argTypes = { + minSearchLength: { name: 'Minimum search start length' }, + maxSuggestions: { name: 'Maximum sugggestions to show' }, + apiUrl: { name: 'URL of the API to use' }, + followedTopicIds: { + type: 'select', + name: 'Followed Topics', + options: { + None: [], + 'World Elephant Water Polo': ['f95d1e16-2307-4feb-b3ff-6f224798aa49'], + 'Brexit, Britain after Brexit, Brexit Unspun Podcast': [ + '19b95057-4614-45fb-9306-4d54049354db', + '464cc2f2-395e-4c36-bb29-01727fc95558', + 'c4e899ed-157e-4446-86f0-5a65803dc07a' + ] + } + }, + csrfToken: { name: 'CSRF Token' } +} From 0fb661411bbce289bd29b91943c5ade5b8ccb4dd Mon Sep 17 00:00:00 2001 From: rowanbeentje Date: Wed, 24 Nov 2021 09:13:59 +0000 Subject: [PATCH 69/71] Update x-topic-search bower dependencies to majoor versions used across the rest of x-dash --- components/x-topic-search/bower.json | 7 ++--- .../x-topic-search/src/TopicSearch.scss | 27 ++++++++++--------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/components/x-topic-search/bower.json b/components/x-topic-search/bower.json index f92919018..9df30a561 100644 --- a/components/x-topic-search/bower.json +++ b/components/x-topic-search/bower.json @@ -3,8 +3,9 @@ "main": "dist/TopicSearch.es5.js", "private": true, "dependencies": { - "o-icons": "^5.8.0", - "o-typography": "^5.7.8", - "o-colors": "^4.7.7" + "o-icons": "^6.3.0", + "o-typography": "^6.4.6", + "o-editorial-typography": "^1.2.1", + "o-colors": "^5.4.1" } } diff --git a/components/x-topic-search/src/TopicSearch.scss b/components/x-topic-search/src/TopicSearch.scss index 8e3bfd5a0..b45f875ed 100644 --- a/components/x-topic-search/src/TopicSearch.scss +++ b/components/x-topic-search/src/TopicSearch.scss @@ -1,6 +1,9 @@ +$system-code:'github:Financial-Times/x-dash' !default; + @import 'o-icons/main'; @import 'o-colors/main'; @import 'o-typography/main'; +@import 'o-editorial-typography/main'; :global { @import "~@financial-times/x-follow-button/dist/FollowButton"; @@ -9,8 +12,8 @@ .container { position: relative; text-align: center; - background-color: oColorsGetPaletteColor('claret-70'); - color: oColorsGetPaletteColor('white'); + background-color: oColorsByName('claret-70'); + color: oColorsByName('white'); width: 100%; } @@ -19,7 +22,7 @@ } .search-icon { - @include oIconsGetIcon('search', oColorsGetPaletteColor('white'), 32); + @include oIconsContent($icon-name: 'search', $color: oColorsByName('white'), $size: 32); position: absolute; top: 4px; left: -7px; @@ -32,18 +35,18 @@ min-height: 40px; margin: 0; border: none; - border-bottom: 2px solid oColorsGetPaletteColor('white'); + border-bottom: 2px solid oColorsByName('white'); padding: 0 9px 0 24px; max-width: none; - color: oColorsGetPaletteColor('white'); + color: oColorsByName('white'); background: transparent; &::placeholder { - color: oColorsGetPaletteColor('white'); + color: oColorsByName('white'); } &::-webkit-search-cancel-button { - @include oIconsGetIcon('cross', oColorsGetPaletteColor('white'), 26); + @include oIconsContent($icon-name: 'cross', $color: oColorsByName('white'), $size: 26); -webkit-appearance: none; } @@ -56,7 +59,7 @@ .result-container { position: absolute; - background: oColorsGetPaletteColor('white'); + background: oColorsByName('white'); top: 48px; padding: 10px; z-index: 1; @@ -76,7 +79,7 @@ align-items: center; clear: right; padding: 5px 0; - border-bottom: 1px solid oColorsGetPaletteColor('black-5'); + border-bottom: 1px solid oColorsByName('black-5'); &:last-child { border-bottom: 0; @@ -90,8 +93,8 @@ } .suggestion__name { - @include oTypographySansBold($scale: -2); - @include oTypographyTopic(); + @include oTypographySans($scale: -2, $weight: 'semibold'); + @include oEditorialTypographyTag($type: 'topic'); padding: 5px 0; max-width: 50%; word-wrap: break-word; @@ -100,7 +103,7 @@ .no-suggestions { @include oTypographySans($scale: 1); - color: oColorsGetPaletteColor('black-70'); + color: oColorsByName('black-70'); text-align: left; padding: 0 0 5px 0; margin: 0; From 9b96d24423ee553f2ebb0d908bcf311d72f13a9f Mon Sep 17 00:00:00 2001 From: rowanbeentje Date: Wed, 24 Nov 2021 09:14:29 +0000 Subject: [PATCH 70/71] Update x-topic-search to use the merged component version of x-follow-button instead of the prerelease branch --- components/x-topic-search/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/x-topic-search/package.json b/components/x-topic-search/package.json index df5555b31..c57e39d9b 100644 --- a/components/x-topic-search/package.json +++ b/components/x-topic-search/package.json @@ -18,7 +18,7 @@ "license": "ISC", "dependencies": { "@financial-times/x-engine": "file:../../packages/x-engine", - "@financial-times/x-follow-button": "^0.0.12", + "@financial-times/x-follow-button": "file:../x-follow-button", "classnames": "^2.2.6", "debounce-promise": "^3.1.0" }, From 8c7f58c293c6c800050160a4693ccdb8e6cdab54 Mon Sep 17 00:00:00 2001 From: rowanbeentje Date: Wed, 24 Nov 2021 09:50:04 +0000 Subject: [PATCH 71/71] Update x-topic-search test setup so that fetchMock is set up in the beforeEach() blocks, to allow afterEach() to tear it down after each use instead of the at-init setup in each describe() block --- .../__tests__/x-topic-search.test.jsx | 151 +++++++++--------- 1 file changed, 76 insertions(+), 75 deletions(-) diff --git a/components/x-topic-search/__tests__/x-topic-search.test.jsx b/components/x-topic-search/__tests__/x-topic-search.test.jsx index 1bbf87aea..fe233cba6 100644 --- a/components/x-topic-search/__tests__/x-topic-search.test.jsx +++ b/components/x-topic-search/__tests__/x-topic-search.test.jsx @@ -1,127 +1,128 @@ -const fetchMock = require('fetch-mock'); -const { h } = require('@financial-times/x-engine'); -const { mount } = require('@financial-times/x-test-utils/enzyme'); -const { TopicSearch } = require('../'); - -const minSearchLength = 3; -const maxSuggestions = 3; -const apiUrl = 'api-url'; -const FOLLOWED_TOPIC_ID1 = 'Cat-House-id'; -const FOLLOWED_TOPIC_ID2 = 'Cat-Food-id'; -const UNFOLLOWED_TOPIC_ID1 = 'Cat-Toys-id'; +const fetchMock = require('fetch-mock') +const { h } = require('@financial-times/x-engine') +const { mount } = require('@financial-times/x-test-utils/enzyme') +const { TopicSearch } = require('../') + +const minSearchLength = 3 +const maxSuggestions = 3 +const apiUrl = 'api-url' +const FOLLOWED_TOPIC_ID1 = 'Cat-House-id' +const FOLLOWED_TOPIC_ID2 = 'Cat-Food-id' +const UNFOLLOWED_TOPIC_ID1 = 'Cat-Toys-id' describe('x-topic-search', () => { - const buildSearchUrl = term => `${apiUrl}?count=${maxSuggestions}&partial=${term}`; - const enterSearchTerm = searchTerm => { - target.find('input').simulate('input', { target: { value: searchTerm }}); + const buildSearchUrl = (term) => `${apiUrl}?count=${maxSuggestions}&partial=${term}` + const enterSearchTerm = (searchTerm) => { + target.find('input').simulate('input', { target: { value: searchTerm } }) - return new Promise(resolve => { setTimeout(resolve, 400); }); - }; - let target; + return new Promise((resolve) => { + setTimeout(resolve, 400) + }) + } + let target beforeEach(() => { const props = { minSearchLength, maxSuggestions, apiUrl, - followedTopicIds: [FOLLOWED_TOPIC_ID1, FOLLOWED_TOPIC_ID2], - }; - target = mount(); - }); + followedTopicIds: [FOLLOWED_TOPIC_ID1, FOLLOWED_TOPIC_ID2] + } + target = mount() + }) afterEach(() => { - fetchMock.reset(); - }); + fetchMock.reset() + }) describe('initial rendering', () => { it('should render with input box', () => { - expect(target.find('input').exists()).toBe(true); - }); + expect(target.find('input').exists()).toBe(true) + }) it('should not display result container', () => { - expect(target.render().children('div')).toHaveLength(1); - }); - }); + expect(target.render().children('div')).toHaveLength(1) + }) + }) describe('when input receives focus', () => { it('selects the text in the input', () => { - const selectMock = jest.fn(); - const inputBox = target.find('input'); + const selectMock = jest.fn() + const inputBox = target.find('input') - inputBox.simulate('blur'); - inputBox.simulate('focus', { target: { select: selectMock }}); + inputBox.simulate('blur') + inputBox.simulate('focus', { target: { select: selectMock } }) - expect(selectMock).toHaveBeenCalledTimes(1); - }); - }); + expect(selectMock).toHaveBeenCalledTimes(1) + }) + }) describe('given inputted text is shorter than minSearchLength', () => { - const apiUrlWithResults = buildSearchUrl('a'); - - fetchMock.get(apiUrlWithResults, []); + const apiUrlWithResults = buildSearchUrl('a') beforeEach(() => { - return enterSearchTerm('a'); - }); + fetchMock.get(apiUrlWithResults, []) + return enterSearchTerm('a') + }) it('does not make a request to the api or render any result', () => { - expect(fetchMock.called(apiUrlWithResults)).toBe(false); - expect(target.render().children('div')).toHaveLength(1); - }); - }); + expect(fetchMock.called(apiUrlWithResults)).toBe(false) + expect(target.render().children('div')).toHaveLength(1) + }) + }) describe('given searchTerm which has some topic suggestions to follow', () => { - const apiUrlWithResults = buildSearchUrl('Cat'); + const apiUrlWithResults = buildSearchUrl('Cat') const results = [ { id: FOLLOWED_TOPIC_ID1, prefLabel: 'Cat House', url: 'Cat-House-url' }, { id: FOLLOWED_TOPIC_ID2, prefLabel: 'Cat Food', url: 'Cat-Food-url' }, { id: UNFOLLOWED_TOPIC_ID1, prefLabel: 'Cat Toys', url: 'Cat-Toys-url' } - ]; - - fetchMock.get(apiUrlWithResults, results); + ] beforeEach(() => { - return enterSearchTerm('Cat'); - }); + fetchMock.get(apiUrlWithResults, results) + return enterSearchTerm('Cat') + }) it('requests the topic suggestions with count set to maxSuggestions', () => { - expect(fetchMock.called(apiUrlWithResults)).toBe(true); - }); + expect(fetchMock.called(apiUrlWithResults)).toBe(true) + }) it('renders no more than the max number of suggestions', () => { - expect(target.render().children('div')).toHaveLength(2); - expect(target.render().find('li')).toHaveLength(maxSuggestions); - }); + expect(target.render().children('div')).toHaveLength(2) + expect(target.render().find('li')).toHaveLength(maxSuggestions) + }) it('renders links and follow buttons for each suggestion', () => { - const suggestionsList = target.render().find('li'); + const suggestionsList = target.render().find('li') results.forEach((topic, index) => { - const suggestion = suggestionsList.eq(index); - - expect(suggestion.find('a').text()).toEqual(topic.prefLabel); - expect(suggestion.find('a').attr('href')).toEqual(topic.url); - expect(suggestion.find('button').text()).toEqual(topic.id === UNFOLLOWED_TOPIC_ID1 ? 'Add to myFT' : 'Added'); - }); + const suggestion = suggestionsList.eq(index) + + expect(suggestion.find('a').text()).toEqual(topic.prefLabel) + expect(suggestion.find('a').attr('href')).toEqual(topic.url) + expect(suggestion.find('button').text()).toEqual( + topic.id === UNFOLLOWED_TOPIC_ID1 ? 'Add to myFT' : 'Added' + ) + }) }) - }); + }) describe('given searchTerm which has no topic suggestions to follow', () => { - const apiUrlNoResults = buildSearchUrl('Dog'); - - fetchMock.get(apiUrlNoResults, []); + const apiUrlNoResults = buildSearchUrl('Dog') beforeEach(() => { - return enterSearchTerm('Dog'); - }); + fetchMock.get(apiUrlNoResults, []) + return enterSearchTerm('Dog') + }) it('requests from the api and renders the no matching topics message', () => { - expect(fetchMock.called(apiUrlNoResults)).toBe(true); + expect(fetchMock.called(apiUrlNoResults)).toBe(true) - const resultContainer = target.render().children('div').eq(1); + const resultContainer = target.render().children('div').eq(1) - expect(resultContainer).toHaveLength(1); - expect(resultContainer.find('h2').text()).toMatch('No topics matching'); - }); - }); -}); + expect(resultContainer).toHaveLength(1) + expect(resultContainer.find('h2').text()).toMatch('No topics matching') + }) + }) +})