@financial-times/n-myft-ui 26.1.0 → 27.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. package/.circleci/config.yml +46 -34
  2. package/.nvmrc +1 -1
  3. package/Makefile +0 -1
  4. package/README.md +2 -48
  5. package/build-state/npm-shrinkwrap.json +10147 -20187
  6. package/components/collections/collections.html +85 -0
  7. package/components/concept-list/concept-list.html +31 -0
  8. package/components/csrf-token/input.html +5 -0
  9. package/components/follow-button/follow-button.html +79 -0
  10. package/components/instant-alert/instant-alert.html +47 -0
  11. package/components/pin-button/pin-button.html +20 -0
  12. package/components/save-for-later/save-for-later.html +68 -0
  13. package/components/unread-articles-indicator/date-fns.js +6 -6
  14. package/demos/app.js +3 -26
  15. package/demos/templates/demo.html +11 -10
  16. package/myft/main.scss +145 -0
  17. package/myft/ui/lists.js +22 -0
  18. package/myft/ui/save-article-to-list-variant.js +369 -0
  19. package/package.json +16 -30
  20. package/components/collections/collections.jsx +0 -68
  21. package/components/collections/collections.test.js +0 -83
  22. package/components/concept-list/concept-list.jsx +0 -69
  23. package/components/concept-list/concept-list.test.js +0 -116
  24. package/components/csrf-token/input.jsx +0 -20
  25. package/components/csrf-token/input.test.js +0 -23
  26. package/components/follow-button/follow-button.jsx +0 -176
  27. package/components/follow-button/follow-button.test.js +0 -40
  28. package/components/index.js +0 -17
  29. package/components/instant-alert/instant-alert.jsx +0 -73
  30. package/components/instant-alert/instant-alert.test.js +0 -86
  31. package/components/pin-button/pin-button.jsx +0 -40
  32. package/components/pin-button/pin-button.test.js +0 -57
  33. package/components/save-for-later/save-for-later.jsx +0 -101
  34. package/components/save-for-later/save-for-later.test.js +0 -59
  35. package/demos/templates/demo-layout.html +0 -25
  36. package/demos/templates/demo.jsx +0 -125
  37. package/dist/bundles/bundle.js +0 -3232
  38. package/jest.config.js +0 -8
  39. package/jsx-migration.md +0 -16
  40. package/webpack.config.js +0 -34
package/myft/ui/lists.js CHANGED
@@ -6,6 +6,7 @@ import nNotification from 'n-notification';
6
6
  import { uuid } from 'n-ui-foundations';
7
7
  import getToken from './lib/get-csrf-token';
8
8
  import oForms from 'o-forms';
9
+ import openSaveArticleToListVariant from './save-article-to-list-variant';
9
10
 
10
11
  const delegate = new Delegate(document.body);
11
12
  const csrfToken = getToken();
@@ -161,6 +162,10 @@ function showArticleSavedOverlay (contentId) {
161
162
  showListsOverlay('Article saved', `/myft/list?fragment=true&fromArticleSaved=true&contentId=${contentId}`, contentId);
162
163
  }
163
164
 
165
+ function showCreateListAndAddArticleOverlay (contentId, name = 'myft-ui-create-list-variant') {
166
+ return openSaveArticleToListVariant(name, contentId);
167
+ }
168
+
164
169
  function handleArticleSaved (contentId) {
165
170
  return myFtClient.getAll('created', 'list')
166
171
  .then(createdLists => createdLists.filter(list => !list.isRedirect))
@@ -171,10 +176,27 @@ function handleArticleSaved (contentId) {
171
176
  });
172
177
  }
173
178
 
179
+ function openCreateListAndAddArticleOverlay (contentId) {
180
+ return myFtClient.getAll('created', 'list')
181
+ .then(createdLists => createdLists.filter(list => !list.isRedirect))
182
+ .then(createdLists => {
183
+ return !createdLists.length ? showCreateListAndAddArticleOverlay(contentId) : showArticleSavedOverlay(contentId);
184
+ });
185
+ }
186
+
174
187
  function initialEventListeners () {
175
188
 
176
189
  document.body.addEventListener('myft.user.saved.content.add', event => {
177
190
  const contentId = event.detail.subject;
191
+
192
+ // Checks if the createListAndSaveArticle variant is active
193
+ // and will show the variant overlay if the user has no lists,
194
+ // otherwise it will show the classic overlay
195
+ const createListVariant = event.currentTarget.querySelector('[data-myft-ui-variant="createListAndSaveArticleVariant"]');
196
+ if (createListVariant) {
197
+ return openCreateListAndAddArticleOverlay(contentId);
198
+ }
199
+
178
200
  handleArticleSaved(contentId);
179
201
  });
180
202
 
@@ -0,0 +1,369 @@
1
+ import Overlay from 'o-overlay';
2
+ import myFtClient from 'next-myft-client';
3
+ import { uuid } from 'n-ui-foundations';
4
+ import getToken from './lib/get-csrf-token';
5
+
6
+ const csrfToken = getToken();
7
+
8
+ let lists;
9
+
10
+ export default async function showSaveArticleToListVariant (name, contentId) {
11
+ try {
12
+ await openSaveArticleToListVariant (name, contentId);
13
+ } catch(error) {
14
+ handleError(error);
15
+ }
16
+ }
17
+
18
+ async function openSaveArticleToListVariant (name, contentId) {
19
+ function createList (list) {
20
+ if(!list) {
21
+ return;
22
+ }
23
+
24
+ myFtClient.add('user', null, 'created', 'list', uuid(), { name: list, token: csrfToken })
25
+ .then(detail => {
26
+ myFtClient.add('list', detail.subject, 'contained', 'content', contentId, { token: csrfToken }).then((createdList) => {
27
+ lists.push({ name: list, uuid: createdList.actorId, checked: true });
28
+ const listElement = ListsElement(lists, addToList, removeFromList);
29
+ const overlayContent = document.querySelector('.o-overlay__content');
30
+ overlayContent.insertAdjacentElement('afterbegin', listElement);
31
+ const announceListContainer = document.querySelector('.myft-ui-create-list-variant-announcement');
32
+ announceListContainer.textContent = `${list} created`;
33
+ triggerCreateListEvent(contentId);
34
+ contentElement.addEventListener('click', openFormHandler, { once: true });
35
+ });
36
+ });
37
+ }
38
+
39
+ function addToList (list) {
40
+ if(!list) {
41
+ return;
42
+ }
43
+
44
+ myFtClient.add('list', list.uuid, 'contained', 'content', contentId, { token: csrfToken }).then(() => {
45
+ const indexToUpdate = lists.indexOf(list);
46
+ lists[indexToUpdate] = { ...lists[indexToUpdate], checked: true };
47
+ const listElement = ListsElement(lists, addToList, removeFromList);
48
+ const overlayContent = document.querySelector('.o-overlay__content');
49
+ overlayContent.insertAdjacentElement('afterbegin', listElement);
50
+ triggerAddToListEvent(contentId);
51
+ });
52
+ }
53
+
54
+ function removeFromList (list) {
55
+ if(!list) {
56
+ return;
57
+ }
58
+
59
+ myFtClient.remove('list', list.uuid, 'contained', 'content', contentId, { token: csrfToken }).then(() => {
60
+ const indexToUpdate = lists.indexOf(list);
61
+ lists[indexToUpdate] = { ...lists[indexToUpdate], checked: false };
62
+ const listElement = ListsElement(lists, addToList, removeFromList);
63
+ const overlayContent = document.querySelector('.o-overlay__content');
64
+ overlayContent.insertAdjacentElement('afterbegin', listElement);
65
+ triggerRemoveFromListEvent(contentId);
66
+ });
67
+ }
68
+
69
+ if (!lists) {
70
+ lists = await getLists(contentId);
71
+ }
72
+
73
+ const overlays = Overlay.getOverlays();
74
+ const existingOverlay = overlays[name];
75
+ if (existingOverlay) {
76
+ existingOverlay.destroy();
77
+ }
78
+
79
+ const headingElement = HeadingElement();
80
+ let [contentElement, removeDescription] = ContentElement(!lists.length);
81
+
82
+ const createListOverlay = new Overlay(name, {
83
+ html: contentElement,
84
+ heading: { title: headingElement.outerHTML },
85
+ modal: false,
86
+ parentnode: isMobile() ? '.o-share--horizontal' : '.o-share--vertical',
87
+ class: 'myft-ui-create-list-variant',
88
+ });
89
+
90
+ const realignListener = realignOverlay(window.scrollY);
91
+
92
+ function outsideClickHandler (e) {
93
+ try {
94
+ const overlayContent = document.querySelector('.o-overlay__content');
95
+ if(!overlayContent || !overlayContent.contains(e.target)) {
96
+ createListOverlay.close();
97
+ }
98
+ } catch(error) {
99
+ handleError(error);
100
+ }
101
+ }
102
+
103
+ function openFormHandler () {
104
+ try {
105
+ const formElement = FormElement(createList);
106
+ const overlayContent = document.querySelector('.o-overlay__content');
107
+ removeDescription();
108
+ overlayContent.insertAdjacentElement('beforeend', formElement);
109
+ formElement.elements[0].focus();
110
+ } catch(error) {
111
+ handleError(error);
112
+ }
113
+ }
114
+
115
+ createListOverlay.open();
116
+ createListOverlay.wrapper.addEventListener('oOverlay.ready', (data) => {
117
+ realignListener(data.currentTarget);
118
+
119
+ if (lists && lists.length) {
120
+ const listElement = ListsElement(lists, addToList, removeFromList);
121
+ const overlayContent = document.querySelector('.o-overlay__content');
122
+ overlayContent.insertAdjacentElement('afterbegin', listElement);
123
+ }
124
+
125
+ contentElement.addEventListener('click', openFormHandler, { once: true });
126
+
127
+ document.querySelector('.article-content').addEventListener('click', outsideClickHandler, { once: true });
128
+ });
129
+
130
+ window.addEventListener('scroll', realignListener(createListOverlay.wrapper, window.scrollY));
131
+
132
+ window.addEventListener('oViewport.resize', () => {
133
+ realignListener(createListOverlay.wrapper);
134
+ });
135
+ }
136
+
137
+ function stringToHTMLElement (string) {
138
+ const template = document.createElement('template');
139
+ template.innerHTML = string.trim();
140
+ return template.content.firstChild;
141
+ }
142
+
143
+ function FormElement (createList) {
144
+ const formString = `
145
+ <form class="myft-ui-create-list-variant-form">
146
+ <label class="o-forms-field">
147
+ <span class="o-forms-input o-forms-input--text">
148
+ <input type="text" name="list-name" aria-label="List name">
149
+ </span>
150
+ </label>
151
+ <button class="o-buttons o-buttons--secondary" type="submit">
152
+ Save
153
+ </button>
154
+ </form>
155
+ `;
156
+
157
+ const formElement = stringToHTMLElement(formString);
158
+
159
+ function handleSubmit (event) {
160
+ try {
161
+ event.preventDefault();
162
+ event.stopPropagation();
163
+ const inputListName = formElement.querySelector('input[name="list-name"]');
164
+ createList(inputListName.value);
165
+ inputListName.value = '';
166
+ formElement.remove();
167
+ } catch(error) {
168
+ handleError(error);
169
+ }
170
+ }
171
+
172
+ formElement.querySelector('button[type="submit"]').addEventListener('click', handleSubmit);
173
+
174
+ return formElement;
175
+ }
176
+
177
+ function ContentElement (description) {
178
+ const content = `
179
+ <div class="myft-ui-create-list-variant-footer">
180
+ <button class="myft-ui-create-list-variant-add">Add to a new list</button>
181
+ ${description ? `
182
+ <p class="myft-ui-create-list-variant-add-description">Lists are a simple way to curate your content</p>
183
+ ` : ''}
184
+ </div>
185
+ `;
186
+
187
+ const contentElement = stringToHTMLElement(content);
188
+
189
+ function removeDescription () {
190
+ const descriptionElement = contentElement.querySelector('.myft-ui-create-list-variant-add-description');
191
+ if (descriptionElement) {
192
+ descriptionElement.remove();
193
+ }
194
+ }
195
+
196
+ return [contentElement, removeDescription];
197
+ }
198
+
199
+ function HeadingElement () {
200
+ const heading = `
201
+ <span class="myft-ui-create-list-variant-heading">Added to <a href="https://www.ft.com/myft/saved-articles">saved articles</a> in <span class="myft-ui-create-list-variant-icon"><span class="myft-ui-create-list-variant-icon-visually-hidden">my FT</span></span></span>
202
+ `;
203
+
204
+ return stringToHTMLElement(heading);
205
+ }
206
+
207
+ function ListsElement (lists, addToList, removeFromList) {
208
+ const currentList = document.querySelector('.myft-ui-create-list-variant-lists');
209
+ if (currentList) {
210
+ currentList.remove();
211
+ }
212
+
213
+ const listCheckboxElement = ListCheckboxElement(addToList, removeFromList);
214
+
215
+ const listsTemplate = `
216
+ <div class="myft-ui-create-list-variant-lists o-forms-field o-forms-field--optional" role="group">
217
+ <span class="myft-ui-create-list-variant-lists-text">Add to a list</span>
218
+ <span class="myft-ui-create-list-variant-lists-container o-forms-input o-forms-input--checkbox">
219
+ </span>
220
+ </div>
221
+ `;
222
+ const listsElement = stringToHTMLElement(listsTemplate);
223
+
224
+ const listsElementContainer = listsElement.querySelector('.myft-ui-create-list-variant-lists-container');
225
+
226
+ lists.map(list => listsElementContainer.insertAdjacentElement('beforeend', listCheckboxElement(list)));
227
+
228
+ return listsElement;
229
+ }
230
+
231
+ function ListCheckboxElement (addToList, removeFromList) {
232
+ return function (list) {
233
+ const listCheckbox = `<label>
234
+ <input type="checkbox" name="default" value="${list.name}" ${list.checked ? 'checked' : ''}>
235
+ <span class="o-forms-input__label">
236
+ <span class="o-normalise-visually-hidden">
237
+ ${list.checked ? 'Remove article from ' : 'Add article to ' }
238
+ </span>
239
+ ${list.name}
240
+ </span>
241
+ </label>
242
+ `;
243
+
244
+ const listCheckboxElement = stringToHTMLElement(listCheckbox);
245
+
246
+ const [ input ] = listCheckboxElement.children;
247
+
248
+ function handleCheck (event) {
249
+ event.preventDefault();
250
+ return event.target.checked ? addToList(list) : removeFromList(list);
251
+ }
252
+
253
+ input.addEventListener('click', handleCheck);
254
+
255
+ return listCheckboxElement;
256
+ };
257
+ }
258
+
259
+ function realignOverlay (originalScrollPosition) {
260
+ return function (target, currentScrollPosition) {
261
+ try {
262
+ if(currentScrollPosition && Math.abs(currentScrollPosition - originalScrollPosition) < 120) {
263
+ return;
264
+ }
265
+
266
+ originalScrollPosition = currentScrollPosition;
267
+
268
+ target.style['min-width'] = '340px';
269
+ target.style['width'] = '100%';
270
+ target.style['margin-top'] = '-50px';
271
+ target.style['left'] = 0;
272
+
273
+ if (isMobile()) {
274
+ target.style['position'] = 'absolute';
275
+ target.style['margin-left'] = 0;
276
+ target.style['margin-top'] = 0;
277
+ target.style['top'] = calculateLargerScreenHalf(target) === 'ABOVE' ? '-120px' : '50px';
278
+ } else {
279
+ target.style['position'] = 'absolute';
280
+ target.style['margin-left'] = '45px';
281
+ target.style['top'] = '220px';
282
+ }
283
+ } catch (error) {
284
+ handleError(error);
285
+ }
286
+ };
287
+ }
288
+
289
+ function isMobile () {
290
+ const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
291
+
292
+ return vw <= 980;
293
+ }
294
+
295
+ function calculateLargerScreenHalf (target) {
296
+ const vh = Math.min(document.documentElement.clientHeight || 0, window.innerHeight || 0);
297
+
298
+ const targetBox = target.getBoundingClientRect();
299
+ const spaceAbove = targetBox.top;
300
+ const spaceBelow = vh - targetBox.bottom;
301
+
302
+ return spaceBelow < spaceAbove ? 'ABOVE' : 'BELOW';
303
+ }
304
+
305
+ async function getLists () {
306
+ return myFtClient.getAll('created', 'list')
307
+ .then(lists => lists.filter(list => !list.isRedirect))
308
+ .then(lists => {
309
+ return lists.map(list => ({ name: list.name, uuid: list.uuid, checked: false }));
310
+ });
311
+ }
312
+
313
+ function triggerAddToListEvent (contentId) {
314
+ return document.body.dispatchEvent(new CustomEvent('oTracking.event', {
315
+ detail: {
316
+ category: 'professorLists',
317
+ action: 'add-to-list',
318
+ article_id: contentId,
319
+ teamName: 'customer-products-us-growth',
320
+ amplitudeExploratory: true
321
+ },
322
+ bubbles: true
323
+ }));
324
+ }
325
+
326
+ function triggerRemoveFromListEvent (contentId) {
327
+ return document.body.dispatchEvent(new CustomEvent('oTracking.event', {
328
+ detail: {
329
+ category: 'professorLists',
330
+ action: 'remove-from-list',
331
+ article_id: contentId,
332
+ teamName: 'customer-products-us-growth',
333
+ amplitudeExploratory: true
334
+ },
335
+ bubbles: true
336
+ }));
337
+ }
338
+
339
+ function triggerCreateListEvent (contentId) {
340
+ document.body.dispatchEvent(new CustomEvent('oTracking.event', {
341
+ detail: {
342
+ category: 'professorLists',
343
+ action: 'create-list',
344
+ article_id: contentId,
345
+ teamName: 'customer-products-us-growth',
346
+ amplitudeExploratory: true
347
+ },
348
+ bubbles: true
349
+ }));
350
+
351
+ return document.body.dispatchEvent(new CustomEvent('oTracking.event', {
352
+ detail: {
353
+ category: 'myFT',
354
+ action: 'create-list-success',
355
+ article_id: contentId
356
+ },
357
+ bubbles: true
358
+ }));
359
+ }
360
+
361
+ function handleError (error) {
362
+ document.body.dispatchEvent(new CustomEvent('oErrors.log', {
363
+ bubbles: true,
364
+ detail: {
365
+ error,
366
+ info: { component: 'professorLists' },
367
+ }
368
+ }));
369
+ }
package/package.json CHANGED
@@ -1,15 +1,13 @@
1
1
  {
2
2
  "name": "@financial-times/n-myft-ui",
3
- "version": "26.1.0",
3
+ "version": "27.2.1",
4
4
  "description": "Client side component for interaction with myft",
5
- "main": "dist/bundles/bundle.js",
6
- "module": "dist/bundles/bundle.js",
5
+ "main": "server.js",
7
6
  "scripts": {
8
7
  "test": "echo \"Error: no test specified\" && exit 1",
9
8
  "commit": "commit-wizard",
10
9
  "prepare": "npx snyk protect || npx snyk protect -d || true",
11
- "preinstall": "[ \"$INIT_CWD\" != \"$PWD\" ] || npm_config_yes=true npx check-engine",
12
- "build-package": "webpack"
10
+ "preinstall": "[ \"$INIT_CWD\" != \"$PWD\" ] || npm_config_yes=true npx check-engine"
13
11
  },
14
12
  "repository": {
15
13
  "type": "git",
@@ -22,18 +20,14 @@
22
20
  },
23
21
  "homepage": "https://github.com/Financial-Times/n-myft-ui#readme",
24
22
  "devDependencies": {
25
- "@financial-times/dotcom-build-bower-resolve": "0.4.1",
26
- "@financial-times/dotcom-build-code-splitting": "0.4.1",
27
- "@financial-times/dotcom-build-js": "0.4.1",
28
- "@financial-times/dotcom-build-sass": "0.4.1",
29
- "@financial-times/dotcom-page-kit-cli": "0.4.1",
30
- "@financial-times/dotcom-server-handlebars": "^3.0.0",
31
- "@financial-times/dotcom-server-react-jsx": "^2.6.2",
32
- "@financial-times/n-express": "^22.4.1",
23
+ "@financial-times/dotcom-build-bower-resolve": "^2.6.2",
24
+ "@financial-times/dotcom-build-code-splitting": "^5.0.0",
25
+ "@financial-times/dotcom-build-js": "^5.0.0",
26
+ "@financial-times/dotcom-build-sass": "^5.0.0",
27
+ "@financial-times/dotcom-page-kit-cli": "^0.6.5",
28
+ "@financial-times/dotcom-server-handlebars": "^5.0.0",
29
+ "@financial-times/n-express": "^23.0.1",
33
30
  "@financial-times/n-gage": "^8.3.2",
34
- "@sucrase/jest-plugin": "^2.2.0",
35
- "@testing-library/jest-dom": "^5.16.1",
36
- "@testing-library/react": "^12.1.2",
37
31
  "ascii-table": "0.0.9",
38
32
  "autoprefixer": "9.7.0",
39
33
  "aws-sdk-mock": "4.5.0",
@@ -46,8 +40,6 @@
46
40
  "babel-plugin-transform-runtime": "^6.9.0",
47
41
  "babel-preset-env": "^1.7.0",
48
42
  "babel-preset-es2015": "^6.6.0",
49
- "babel-preset-react": "^6.24.1",
50
- "babel-preset-stage-2": "^6.24.1",
51
43
  "babel-runtime": "^6.9.2",
52
44
  "bower": "^1.8.8",
53
45
  "bower-resolve-webpack-plugin": "^1.0.5",
@@ -59,18 +51,14 @@
59
51
  "css-loader": "^0.23.1",
60
52
  "denodeify": "^1.2.1",
61
53
  "eslint": "6.5.1",
62
- "eslint-plugin-react": "^7.27.1",
63
54
  "extract-css-block-webpack-plugin": "^1.3.0",
64
- "extract-text-webpack-plugin": "3.0.2",
65
55
  "fetch-mock": "^5.0.3",
66
56
  "handlebars": "^4.0.6",
67
57
  "handlebars-loader": "^1.4.0",
68
58
  "http-server": "^0.11.1",
69
59
  "hyperons": "^0.4.1",
70
60
  "imports-loader": "0.8.0",
71
- "inject-loader": "^3.0.0",
72
- "jest": "^27.4.5",
73
- "jsdom": "^19.0.0",
61
+ "inject-loader": "^4.0.1",
74
62
  "karma": "4.4.1",
75
63
  "karma-browserstack-launcher": "1.5.1",
76
64
  "karma-chai": "^0.1.0",
@@ -81,31 +69,29 @@
81
69
  "karma-sinon": "^1.0.5",
82
70
  "karma-sinon-chai": "2.0.2",
83
71
  "karma-sourcemap-loader": "^0.3.7",
84
- "karma-webpack": "^3.0.0",
72
+ "karma-webpack": "^4.0.2",
85
73
  "lintspaces-cli": "^0.7.0",
86
74
  "lolex": "5.1.1",
87
75
  "mocha": "6.2.2",
88
76
  "mockery": "2.1.0",
89
77
  "node-fetch": "2.6.0",
90
- "node-sass": "^4.14.1",
78
+ "node-sass": "^6.0.1",
91
79
  "nodemon": "^1.9.2",
92
80
  "npm-prepublish": "^1.2.1",
93
81
  "pa11y-ci": "^2.1.1",
94
82
  "postcss-loader": "^0.9.1",
95
- "react": "^17.0.2",
96
83
  "regenerator-runtime": "^0.13.3",
97
84
  "semver": "6.3.0",
98
85
  "sinon": "^7.1.0",
99
86
  "sinon-chai": "^3.2.0",
100
- "snyk": "^1.216.5",
101
- "sucrase": "^3.10.1"
87
+ "snyk": "^1.216.5"
102
88
  },
103
89
  "volta": {
104
- "node": "12.22.5",
90
+ "node": "16.14.2",
105
91
  "npm": "7.20.2"
106
92
  },
107
93
  "engines": {
108
- "node": "12.x",
94
+ "node": "14.x || 16.x",
109
95
  "npm": "7.x || 8.x"
110
96
  },
111
97
  "x-dash": {
@@ -1,68 +0,0 @@
1
- import React from 'react';
2
- import CsrfToken from '../csrf-token/input';
3
- import FollowButton from '../follow-button/follow-button';
4
-
5
- export default function Collections ({ title, liteStyle, flags, collectionName, trackable, concepts = [], csrfToken, cacheablePersonalisedUrl }) {
6
- const getLiteStyleModifier = () => liteStyle ? 'lite' : 'regular';
7
- let formProps = {
8
- method: 'POST',
9
- action: '#',
10
- 'data-myft-ui': 'follow',
11
- 'data-concept-id': concepts.map(concept => concept.id).join(',')
12
- };
13
-
14
- if (collectionName) {
15
- formProps['data-myft-tracking'] = collectionName;
16
- }
17
-
18
- return (
19
- <section
20
- className={`collection collection--${getLiteStyleModifier()}`}
21
- data-trackable={trackable ? trackable : 'collection'}>
22
- <header className={`collection__header collection__header--${getLiteStyleModifier()}`}>
23
- <h2 className={`collection__title collection__title--${getLiteStyleModifier()}`}>
24
- {title}
25
- </h2>
26
- </header>
27
- <ul className="collection__concepts">
28
- {concepts && concepts.map((concept, index) =>
29
- <li className="collection__concept" key={index}>
30
- <FollowButton cacheablePersonalisedUrl={cacheablePersonalisedUrl} csrfToken={csrfToken} variant={liteStyle ? 'primary' : 'inverse'} buttonText={concept.name} flags={flags} collectionName={collectionName} />
31
- </li>)
32
- }
33
- </ul>
34
-
35
- <div className="collection__meta">
36
- <form
37
- {...formProps}
38
- className="n-myft-ui n-myft-ui--follow n-ui-hide-core collection-follow-all">
39
- <input
40
- type="hidden"
41
- name="directType"
42
- value={concepts.map(concept => concept.directType).join(',')}
43
- />
44
- <CsrfToken csrfToken={csrfToken} cacheablePersonalisedUrl={cacheablePersonalisedUrl} />
45
- <input
46
- type="hidden"
47
- name="name"
48
- value={concepts.map(concept => concept.name).join(',')}
49
- />
50
- <button
51
- type="submit"
52
- aria-pressed="false"
53
- className={`collection-follow-all__button collection-follow-all__button--${getLiteStyleModifier()}`}
54
- data-trackable="follow all"
55
- data-concept-id={concepts.map(concept => concept.id).join(',')}
56
- aria-label={`Add all topics in the ${title} collection to my F T`}
57
- data-alternate-label={`Remove all topics in the ${title} collection from my F T`}
58
- data-alternate-text="Added"
59
- title={`Add all topics in the ${title} collection to my F T`}>
60
- Add all to myFT
61
- </button>
62
- </form>
63
- </div>
64
- </section>
65
-
66
- )
67
-
68
- }
@@ -1,83 +0,0 @@
1
- import React from 'react';
2
- import Collections from './collections';
3
- import { render, screen } from '@testing-library/react';
4
- import '@testing-library/jest-dom';
5
-
6
- const fixtures = {
7
- 'title': 'European Union',
8
- 'concepts': [
9
- {
10
- 'id': '00000000-0000-0000-0000-000000000000',
11
- 'prefLabel': 'EU immigration',
12
- 'directType': 'http://www.ft.com/ontology/Topic',
13
- 'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000000',
14
- 'name': 'EU immigration'
15
- },
16
- {
17
- 'id': '00000000-0000-0000-0000-000000000001',
18
- 'prefLabel': 'Europe Quantitative Easing',
19
- 'directType': 'http://www.ft.com/ontology/Topic',
20
- 'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000000',
21
- 'name': 'Europe Quantitative Easing'
22
- },
23
- {
24
- 'id': '00000000-0000-0000-0000-000000000002',
25
- 'prefLabel': 'EU financial regulation',
26
- 'directType': 'http://www.ft.com/ontology/Topic',
27
- 'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000000',
28
- 'name': 'EU financial regulation'
29
- },
30
- {
31
- 'id': '00000000-0000-0000-0000-000000000003',
32
- 'prefLabel': 'EU nothing',
33
- 'directType': 'http://www.ft.com/ontology/Topic',
34
- 'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000000',
35
- 'name': 'EU nothing'
36
- },
37
- {
38
- 'id': '00000000-0000-0000-0000-000000000004',
39
- 'prefLabel': 'EU trade',
40
- 'directType': 'http://www.ft.com/ontology/Topic',
41
- 'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000000',
42
- 'name': 'EU trade'
43
- }
44
- ]
45
- };
46
- const joinedDirectTypes = 'http://www.ft.com/ontology/Topic,http://www.ft.com/ontology/Topic,http://www.ft.com/ontology/Topic,http://www.ft.com/ontology/Topic,http://www.ft.com/ontology/Topic';
47
-
48
- const flags = {
49
- myFtApi: true,
50
- myFtApiWrite: true
51
- };
52
-
53
- describe('Concept List', () => {
54
-
55
- test('It renders the title of the collection', async () => {
56
- render(<Collections {...fixtures} flags={flags} />);
57
- expect(await screen.findByText('European Union')).toBeTruthy();
58
- });
59
-
60
- test('It renders label for the concept button', async () => {
61
- render(<Collections {...fixtures} flags={flags} />);
62
- expect(await screen.findByText('EU immigration')).toBeTruthy();
63
- expect(await screen.findByText('Europe Quantitative Easing')).toBeTruthy();
64
- expect(await screen.findByText('EU financial regulation')).toBeTruthy();
65
- expect(await screen.findByText('EU nothing')).toBeTruthy();
66
- expect(await screen.findByText('EU trade')).toBeTruthy();
67
- });
68
-
69
- test('It renders form "Add all to my FT" from', () => {
70
- const { container} = render(<Collections {...fixtures} flags={flags} />);
71
- const formElement = container.querySelector('form[action="#"]');
72
- expect(formElement).toBeTruthy();
73
- expect(formElement.method).toEqual('post');
74
- });
75
-
76
- test('It renders directType input with value of types joined', () => {
77
- const { container} = render(<Collections {...fixtures} flags={flags} />);
78
- const directTypeElement = container.querySelector('input[name="directType"]');
79
- expect(directTypeElement).toBeTruthy();
80
- expect(directTypeElement.value).toEqual(joinedDirectTypes);
81
- });
82
-
83
- });