@financial-times/n-myft-ui 28.0.7 → 28.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -15,7 +15,7 @@
15
15
  "form-serialize": "^0.7.2",
16
16
  "ftdomdelegate": "^4.0.6",
17
17
  "js-cookie": "^2.2.1",
18
- "next-myft-client": "^10.1.0",
18
+ "next-myft-client": "^10.3.0",
19
19
  "next-session-client": "^4.0.0",
20
20
  "superstore-sync": "^2.1.1"
21
21
  },
@@ -65,6 +65,7 @@
65
65
  "karma-sinon": "^1.0.5",
66
66
  "karma-sinon-chai": "2.0.2",
67
67
  "karma-sourcemap-loader": "^0.3.7",
68
+ "karma-viewport": "^1.0.9",
68
69
  "karma-webpack": "^4.0.2",
69
70
  "lintspaces-cli": "^0.7.0",
70
71
  "lolex": "5.1.1",
@@ -3383,6 +3384,84 @@
3383
3384
  "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
3384
3385
  "dev": true
3385
3386
  },
3387
+ "node_modules/@types/karma": {
3388
+ "version": "6.3.3",
3389
+ "resolved": "https://registry.npmjs.org/@types/karma/-/karma-6.3.3.tgz",
3390
+ "integrity": "sha512-nRMec4mTCt+tkpRqh5/pAxmnjzEgAaalIq7mdfLFH88gSRC8+bxejLiSjHMMT/vHIhJHqg4GPIGCnCFbwvDRww==",
3391
+ "dev": true,
3392
+ "dependencies": {
3393
+ "@types/node": "*",
3394
+ "log4js": "^6.4.1"
3395
+ }
3396
+ },
3397
+ "node_modules/@types/karma/node_modules/date-format": {
3398
+ "version": "4.0.13",
3399
+ "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.13.tgz",
3400
+ "integrity": "sha512-bnYCwf8Emc3pTD8pXnre+wfnjGtfi5ncMDKy7+cWZXbmRAsdWkOQHrfC1yz/KiwP5thDp2kCHWYWKBX4HP1hoQ==",
3401
+ "dev": true,
3402
+ "engines": {
3403
+ "node": ">=4.0"
3404
+ }
3405
+ },
3406
+ "node_modules/@types/karma/node_modules/flatted": {
3407
+ "version": "3.2.6",
3408
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.6.tgz",
3409
+ "integrity": "sha512-0sQoMh9s0BYsm+12Huy/rkKxVu4R1+r96YX5cG44rHV0pQ6iC3Q+mkoMFaGWObMFYQxCVT+ssG1ksneA2MI9KQ==",
3410
+ "dev": true
3411
+ },
3412
+ "node_modules/@types/karma/node_modules/fs-extra": {
3413
+ "version": "8.1.0",
3414
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
3415
+ "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
3416
+ "dev": true,
3417
+ "dependencies": {
3418
+ "graceful-fs": "^4.2.0",
3419
+ "jsonfile": "^4.0.0",
3420
+ "universalify": "^0.1.0"
3421
+ },
3422
+ "engines": {
3423
+ "node": ">=6 <7 || >=8"
3424
+ }
3425
+ },
3426
+ "node_modules/@types/karma/node_modules/jsonfile": {
3427
+ "version": "4.0.0",
3428
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
3429
+ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
3430
+ "dev": true,
3431
+ "optionalDependencies": {
3432
+ "graceful-fs": "^4.1.6"
3433
+ }
3434
+ },
3435
+ "node_modules/@types/karma/node_modules/log4js": {
3436
+ "version": "6.6.1",
3437
+ "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.6.1.tgz",
3438
+ "integrity": "sha512-J8VYFH2UQq/xucdNu71io4Fo+purYYudyErgBbswWKO0MC6QVOERRomt5su/z6d3RJSmLyTGmXl3Q/XjKCf+/A==",
3439
+ "dev": true,
3440
+ "dependencies": {
3441
+ "date-format": "^4.0.13",
3442
+ "debug": "^4.3.4",
3443
+ "flatted": "^3.2.6",
3444
+ "rfdc": "^1.3.0",
3445
+ "streamroller": "^3.1.2"
3446
+ },
3447
+ "engines": {
3448
+ "node": ">=8.0"
3449
+ }
3450
+ },
3451
+ "node_modules/@types/karma/node_modules/streamroller": {
3452
+ "version": "3.1.2",
3453
+ "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.2.tgz",
3454
+ "integrity": "sha512-wZswqzbgGGsXYIrBYhOE0yP+nQ6XRk7xDcYwuQAGTYXdyAUmvgVFE0YU1g5pvQT0m7GBaQfYcSnlHbapuK0H0A==",
3455
+ "dev": true,
3456
+ "dependencies": {
3457
+ "date-format": "^4.0.13",
3458
+ "debug": "^4.3.4",
3459
+ "fs-extra": "^8.1.0"
3460
+ },
3461
+ "engines": {
3462
+ "node": ">=8.0"
3463
+ }
3464
+ },
3386
3465
  "node_modules/@types/mdast": {
3387
3466
  "version": "3.0.10",
3388
3467
  "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz",
@@ -12267,6 +12346,15 @@
12267
12346
  "graceful-fs": "^4.1.6"
12268
12347
  }
12269
12348
  },
12349
+ "node_modules/jsonschema": {
12350
+ "version": "1.4.1",
12351
+ "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.4.1.tgz",
12352
+ "integrity": "sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ==",
12353
+ "dev": true,
12354
+ "engines": {
12355
+ "node": "*"
12356
+ }
12357
+ },
12270
12358
  "node_modules/jsprim": {
12271
12359
  "version": "1.4.2",
12272
12360
  "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz",
@@ -12441,6 +12529,16 @@
12441
12529
  "graceful-fs": "^4.1.2"
12442
12530
  }
12443
12531
  },
12532
+ "node_modules/karma-viewport": {
12533
+ "version": "1.0.9",
12534
+ "resolved": "https://registry.npmjs.org/karma-viewport/-/karma-viewport-1.0.9.tgz",
12535
+ "integrity": "sha512-E1xVe66vBQtI66TGOtZMzV5nf6BW5tW4TQVUqPK+oakVLdsG/ZUG688tGK0lL1q0t7nfQD1dwLD8Z9Guu/RVdg==",
12536
+ "dev": true,
12537
+ "dependencies": {
12538
+ "@types/karma": "^6.3.3",
12539
+ "jsonschema": "^1.4.0"
12540
+ }
12541
+ },
12444
12542
  "node_modules/karma-webpack": {
12445
12543
  "version": "4.0.2",
12446
12544
  "resolved": "https://registry.npmjs.org/karma-webpack/-/karma-webpack-4.0.2.tgz",
@@ -14265,9 +14363,9 @@
14265
14363
  }
14266
14364
  },
14267
14365
  "node_modules/next-myft-client": {
14268
- "version": "10.1.0",
14269
- "resolved": "https://registry.npmjs.org/next-myft-client/-/next-myft-client-10.1.0.tgz",
14270
- "integrity": "sha512-PqnNE1gxV00yVv6pA+1jWaySJkonQBUx2uWchPNGNcB20SzVmYleSWLeD3bdxdNOD5Tpy8DDl58CEQrKja7bqA==",
14366
+ "version": "10.3.0",
14367
+ "resolved": "https://registry.npmjs.org/next-myft-client/-/next-myft-client-10.3.0.tgz",
14368
+ "integrity": "sha512-dAaIs6PhvYGczEsOHGNrI3FKKFDOcm6rSv06SaL9emltqlHqUXt7esun8m4N3D2iL2FzVXKLeMd9/gv/GDryOg==",
14271
14369
  "hasInstallScript": true,
14272
14370
  "dependencies": {
14273
14371
  "black-hole-stream": "0.0.1",
@@ -25822,6 +25920,74 @@
25822
25920
  "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
25823
25921
  "dev": true
25824
25922
  },
25923
+ "@types/karma": {
25924
+ "version": "6.3.3",
25925
+ "resolved": "https://registry.npmjs.org/@types/karma/-/karma-6.3.3.tgz",
25926
+ "integrity": "sha512-nRMec4mTCt+tkpRqh5/pAxmnjzEgAaalIq7mdfLFH88gSRC8+bxejLiSjHMMT/vHIhJHqg4GPIGCnCFbwvDRww==",
25927
+ "dev": true,
25928
+ "requires": {
25929
+ "@types/node": "*",
25930
+ "log4js": "^6.4.1"
25931
+ },
25932
+ "dependencies": {
25933
+ "date-format": {
25934
+ "version": "4.0.13",
25935
+ "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.13.tgz",
25936
+ "integrity": "sha512-bnYCwf8Emc3pTD8pXnre+wfnjGtfi5ncMDKy7+cWZXbmRAsdWkOQHrfC1yz/KiwP5thDp2kCHWYWKBX4HP1hoQ==",
25937
+ "dev": true
25938
+ },
25939
+ "flatted": {
25940
+ "version": "3.2.6",
25941
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.6.tgz",
25942
+ "integrity": "sha512-0sQoMh9s0BYsm+12Huy/rkKxVu4R1+r96YX5cG44rHV0pQ6iC3Q+mkoMFaGWObMFYQxCVT+ssG1ksneA2MI9KQ==",
25943
+ "dev": true
25944
+ },
25945
+ "fs-extra": {
25946
+ "version": "8.1.0",
25947
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
25948
+ "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
25949
+ "dev": true,
25950
+ "requires": {
25951
+ "graceful-fs": "^4.2.0",
25952
+ "jsonfile": "^4.0.0",
25953
+ "universalify": "^0.1.0"
25954
+ }
25955
+ },
25956
+ "jsonfile": {
25957
+ "version": "4.0.0",
25958
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
25959
+ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
25960
+ "dev": true,
25961
+ "requires": {
25962
+ "graceful-fs": "^4.1.6"
25963
+ }
25964
+ },
25965
+ "log4js": {
25966
+ "version": "6.6.1",
25967
+ "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.6.1.tgz",
25968
+ "integrity": "sha512-J8VYFH2UQq/xucdNu71io4Fo+purYYudyErgBbswWKO0MC6QVOERRomt5su/z6d3RJSmLyTGmXl3Q/XjKCf+/A==",
25969
+ "dev": true,
25970
+ "requires": {
25971
+ "date-format": "^4.0.13",
25972
+ "debug": "^4.3.4",
25973
+ "flatted": "^3.2.6",
25974
+ "rfdc": "^1.3.0",
25975
+ "streamroller": "^3.1.2"
25976
+ }
25977
+ },
25978
+ "streamroller": {
25979
+ "version": "3.1.2",
25980
+ "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.2.tgz",
25981
+ "integrity": "sha512-wZswqzbgGGsXYIrBYhOE0yP+nQ6XRk7xDcYwuQAGTYXdyAUmvgVFE0YU1g5pvQT0m7GBaQfYcSnlHbapuK0H0A==",
25982
+ "dev": true,
25983
+ "requires": {
25984
+ "date-format": "^4.0.13",
25985
+ "debug": "^4.3.4",
25986
+ "fs-extra": "^8.1.0"
25987
+ }
25988
+ }
25989
+ }
25990
+ },
25825
25991
  "@types/mdast": {
25826
25992
  "version": "3.0.10",
25827
25993
  "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz",
@@ -33156,6 +33322,12 @@
33156
33322
  "universalify": "^0.1.2"
33157
33323
  }
33158
33324
  },
33325
+ "jsonschema": {
33326
+ "version": "1.4.1",
33327
+ "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.4.1.tgz",
33328
+ "integrity": "sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ==",
33329
+ "dev": true
33330
+ },
33159
33331
  "jsprim": {
33160
33332
  "version": "1.4.2",
33161
33333
  "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz",
@@ -33415,6 +33587,16 @@
33415
33587
  "graceful-fs": "^4.1.2"
33416
33588
  }
33417
33589
  },
33590
+ "karma-viewport": {
33591
+ "version": "1.0.9",
33592
+ "resolved": "https://registry.npmjs.org/karma-viewport/-/karma-viewport-1.0.9.tgz",
33593
+ "integrity": "sha512-E1xVe66vBQtI66TGOtZMzV5nf6BW5tW4TQVUqPK+oakVLdsG/ZUG688tGK0lL1q0t7nfQD1dwLD8Z9Guu/RVdg==",
33594
+ "dev": true,
33595
+ "requires": {
33596
+ "@types/karma": "^6.3.3",
33597
+ "jsonschema": "^1.4.0"
33598
+ }
33599
+ },
33418
33600
  "karma-webpack": {
33419
33601
  "version": "4.0.2",
33420
33602
  "resolved": "https://registry.npmjs.org/karma-webpack/-/karma-webpack-4.0.2.tgz",
@@ -34770,9 +34952,9 @@
34770
34952
  }
34771
34953
  },
34772
34954
  "next-myft-client": {
34773
- "version": "10.1.0",
34774
- "resolved": "https://registry.npmjs.org/next-myft-client/-/next-myft-client-10.1.0.tgz",
34775
- "integrity": "sha512-PqnNE1gxV00yVv6pA+1jWaySJkonQBUx2uWchPNGNcB20SzVmYleSWLeD3bdxdNOD5Tpy8DDl58CEQrKja7bqA==",
34955
+ "version": "10.3.0",
34956
+ "resolved": "https://registry.npmjs.org/next-myft-client/-/next-myft-client-10.3.0.tgz",
34957
+ "integrity": "sha512-dAaIs6PhvYGczEsOHGNrI3FKKFDOcm6rSv06SaL9emltqlHqUXt7esun8m4N3D2iL2FzVXKLeMd9/gv/GDryOg==",
34776
34958
  "requires": {
34777
34959
  "black-hole-stream": "0.0.1",
34778
34960
  "fetchres": "^1.7.2",
@@ -4,7 +4,7 @@
4
4
  data-myft-ui="saved"
5
5
  action="/myft/save/{{contentId}}"
6
6
  data-js-action="/__myft/api/core/saved/content/{{contentId}}?method=put"
7
- {{#ifEquals @root.flags.professorLists 'variant'}}data-myft-ui-variant="createListAndSaveArticleVariant"{{/ifEquals}}>
7
+ {{#if @root.flags.manageArticleLists}}data-myft-ui-save-new="manageArticleLists"{{/if}}>
8
8
  {{> n-myft-ui/components/csrf-token/input}}
9
9
  <div
10
10
  class="n-myft-ui__announcement o-normalise-visually-hidden"
package/karma.conf.js CHANGED
@@ -10,7 +10,7 @@ module.exports = function (karma) {
10
10
 
11
11
  // frameworks to use
12
12
  // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
13
- frameworks: ['mocha', 'chai', 'sinon', 'sinon-chai'],
13
+ frameworks: ['mocha', 'chai', 'sinon', 'sinon-chai', 'viewport'],
14
14
 
15
15
  // list of files / patterns to load in the browser
16
16
  files: [
@@ -110,7 +110,8 @@ module.exports = function (karma) {
110
110
  require('karma-webpack'),
111
111
  require('karma-chrome-launcher'),
112
112
  require('karma-browserstack-launcher'),
113
- require('karma-html-reporter')
113
+ require('karma-html-reporter'),
114
+ require('karma-viewport')
114
115
  ],
115
116
  client: {
116
117
  mocha: {
package/myft/main.scss CHANGED
@@ -314,7 +314,43 @@ $spacing-unit: 20px;
314
314
  }
315
315
  &-container {
316
316
  margin-top: 0;
317
+ max-height: 92px;
318
+ padding-bottom: 2px;
319
+ overflow-y: auto;
320
+ @include oGridRespondTo($from: M) {
321
+ max-height: 126px;
322
+ }
317
323
  }
318
324
  }
319
325
  }
326
+
327
+ .myft-notification {
328
+ background: oColorsByName('white-80');
329
+ box-sizing: border-box;
330
+ display: flex;
331
+ align-items: center;
332
+ justify-content: center;
333
+ position: absolute;
334
+ border-radius: 5px;
335
+ font-family: MetricWeb, sans-serif;
336
+ font-size: 18px;
337
+ }
338
+
339
+ .share-nav__vertical {
340
+ .myft-notification {
341
+ top: 175px;
342
+ width: 340px;
343
+ height: 44px;
344
+ left: 50px;
345
+ }
346
+ }
347
+
348
+ .share-nav__horizontal {
349
+ .myft-notification {
350
+ top: -52px;
351
+ width: 340px;
352
+ height: 44px;
353
+ z-index: 10;
354
+ }
355
+ }
320
356
  }
@@ -0,0 +1,5 @@
1
+ export default function stringToHTMLElement (string) {
2
+ const template = document.createElement('template');
3
+ template.innerHTML = string.trim();
4
+ return template.content.firstChild;
5
+ }
@@ -0,0 +1,7 @@
1
+ const MOBILE_BREAKPOINT = 980;
2
+
3
+ export default function isMobile () {
4
+ const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
5
+
6
+ return vw <= MOBILE_BREAKPOINT;
7
+ }
@@ -11,12 +11,6 @@ const relationshipConfig = {
11
11
  subjectType: 'concept',
12
12
  uiSelector: '[data-myft-ui="follow"]'
13
13
  },
14
- contained: {
15
- actorType: 'list',
16
- idProperty: 'data-content-id',
17
- subjectType: 'content',
18
- uiSelector: '[data-myft-ui="contained"]'
19
- }
20
14
  };
21
15
 
22
16
  export default relationshipConfig;
@@ -39,12 +39,10 @@ const getExtraContext = (subjectType, subjectId) => {
39
39
  * @param {Object} postedData Event's extra data (required for checking if an instant alert was turned on or off)
40
40
  * @return {String} label for the action to send in the custom event
41
41
  */
42
- const getAction = (subjectType, action, postedData, resultData) => {
42
+ const getAction = (subjectType, action, postedData) => {
43
43
  if (action === 'update' && subjectType === 'concept') {
44
44
  const updateState = (postedData && postedData._rel && postedData._rel.instant && postedData._rel.instant === 'true') ? 'on' : 'off';
45
45
  return `instant-alert-${updateState}`;
46
- } else if (resultData && resultData.rel && resultData.rel.type && resultData.rel.type === 'contained') {
47
- return `${action}-to-list-success`;
48
46
  } else {
49
47
  return customDataSettings[subjectType][action];
50
48
  }
@@ -57,7 +55,7 @@ const getAction = (subjectType, action, postedData, resultData) => {
57
55
  export function custom (eventData) {
58
56
  if (Object.keys(customDataSettings).indexOf(eventData.subjectType) !== -1) {
59
57
  const options = Object.assign(
60
- {action: getAction(eventData.subjectType, eventData.action, eventData.postedData, eventData.resultData)},
58
+ {action: getAction(eventData.subjectType, eventData.action, eventData.postedData)},
61
59
  eventData.trackingInfo);
62
60
  const extraContext = getExtraContext(eventData.subjectType, eventData.subjectId);
63
61
  Object.assign(options, extraContext);
package/myft/ui/lists.js CHANGED
@@ -5,8 +5,10 @@ import * as myFtUiButtonStates from './lib/button-states';
5
5
  import nNotification from '@financial-times/n-notification';
6
6
  import { uuid } from 'n-ui-foundations';
7
7
  import getToken from './lib/get-csrf-token';
8
+ import isMobile from './lib/is-mobile';
8
9
  import oForms from '@financial-times/o-forms';
9
10
  import openSaveArticleToListVariant from './save-article-to-list-variant';
11
+ import stringToHTMLElement from './lib/convert-string-to-html-element';
10
12
 
11
13
  const delegate = new Delegate(document.body);
12
14
  const csrfToken = getToken();
@@ -51,6 +53,18 @@ function updateAfterAddToList (listId, contentId, wasAdded) {
51
53
  content: message,
52
54
  trackable: 'myft-feedback-notification'
53
55
  });
56
+
57
+ document.body.dispatchEvent(new CustomEvent('oTracking.event', {
58
+ detail: {
59
+ category: 'list',
60
+ action: 'copy-success',
61
+ article_id: contentId,
62
+ list_id: listId,
63
+ teamName: 'customer-products-us-growth',
64
+ amplitudeExploratory: true
65
+ },
66
+ bubbles: true
67
+ }));
54
68
  });
55
69
  }
56
70
 
@@ -83,6 +97,10 @@ function setUpCreateListListeners (overlay, contentId) {
83
97
  const createListButton = overlay.content.querySelector('.js-create-list');
84
98
  const nameInput = overlay.content.querySelector('.js-name');
85
99
 
100
+ if (!createListButton) {
101
+ return;
102
+ }
103
+
86
104
  createListButton.addEventListener('click', event => {
87
105
  event.preventDefault();
88
106
 
@@ -179,8 +197,65 @@ function handleArticleSaved (contentId) {
179
197
  function openCreateListAndAddArticleOverlay (contentId) {
180
198
  return myFtClient.getAll('created', 'list')
181
199
  .then(createdLists => createdLists.filter(list => !list.isRedirect))
182
- .then(createdLists => {
183
- return !createdLists.length ? showCreateListAndAddArticleOverlay(contentId) : showArticleSavedOverlay(contentId);
200
+ .then(() => {
201
+ return showCreateListAndAddArticleOverlay(contentId);
202
+ });
203
+ }
204
+
205
+ function handleRemoveToggleSubmit (event) {
206
+ event.preventDefault();
207
+
208
+ const formEl = event.target;
209
+ const submitBtnEl = formEl.querySelector('button[type="submit"]');
210
+
211
+ if (submitBtnEl.hasAttribute('disabled')) {
212
+ return;
213
+ }
214
+
215
+ const isSubmitBtnPressed = submitBtnEl.getAttribute('aria-pressed') === 'true';
216
+ const action = isSubmitBtnPressed ? 'remove' : 'add';
217
+ const contentId = formEl.dataset.contentId;
218
+ const listId = formEl.dataset.actorId;
219
+ const csrfToken = formEl.elements.token;
220
+
221
+ if (!csrfToken || !csrfToken.value) {
222
+ document.body.dispatchEvent(new CustomEvent('oErrors.log', {
223
+ bubbles: true,
224
+ detail: {
225
+ error: new Error('myFT form submitted without a CSRF token'),
226
+ info: {
227
+ action,
228
+ actorType: 'list',
229
+ actorId: listId,
230
+ relationshipName: 'contained',
231
+ subjectType: 'content',
232
+ subjectId: contentId,
233
+ },
234
+ },
235
+ }));
236
+ }
237
+
238
+ submitBtnEl.setAttribute('disabled', '');
239
+
240
+ myFtClient[action]('list', listId, 'contained', 'content', contentId, { token: csrfToken.value })
241
+ .then(() => {
242
+ myFtUiButtonStates.toggleButton(submitBtnEl, !isSubmitBtnPressed);
243
+
244
+ document.body.dispatchEvent(new CustomEvent('oTracking.event', {
245
+ detail: {
246
+ category: 'list',
247
+ action: action === 'add' ? 'add-success' : 'remove-success',
248
+ article_id: contentId,
249
+ list_id: listId,
250
+ teamName: 'customer-products-us-growth',
251
+ amplitudeExploratory: true
252
+ },
253
+ bubbles: true
254
+ }));
255
+ })
256
+ .catch(error => {
257
+ setTimeout(() => submitBtnEl.removeAttribute('disabled'));
258
+ throw error;
184
259
  });
185
260
  }
186
261
 
@@ -192,14 +267,23 @@ function initialEventListeners () {
192
267
  // Checks if the createListAndSaveArticle variant is active
193
268
  // and will show the variant overlay if the user has no lists,
194
269
  // otherwise it will show the classic overlay
195
- const createListVariant = event.currentTarget.querySelector('[data-myft-ui-variant="createListAndSaveArticleVariant"]');
196
- if (createListVariant) {
270
+ const newListDesign = event.currentTarget.querySelector('[data-myft-ui-save-new="manageArticleLists"]');
271
+ if (newListDesign) {
197
272
  return openCreateListAndAddArticleOverlay(contentId);
198
273
  }
199
274
 
200
275
  handleArticleSaved(contentId);
201
276
  });
202
277
 
278
+ document.body.addEventListener('myft.user.saved.content.remove', event => {
279
+ const contentId = event.detail.subject;
280
+
281
+ const newListDesign = event.currentTarget.querySelector('[data-myft-ui-save-new="manageArticleLists"]');
282
+ if (newListDesign) {
283
+ return showUnsavedNotification(contentId);
284
+ }
285
+ });
286
+
203
287
  delegate.on('click', '[data-myft-ui="copy-to-list"]', event => {
204
288
  event.preventDefault();
205
289
  showCopyToListOverlay(event.target.getAttribute('data-content-id'), event.target.getAttribute('data-actor-id'));
@@ -208,6 +292,36 @@ function initialEventListeners () {
208
292
  ev.preventDefault();
209
293
  showCreateListOverlay();
210
294
  });
295
+
296
+ delegate.on('submit', '[data-myft-ui="contained"]', handleRemoveToggleSubmit);
297
+ }
298
+
299
+ function showUnsavedNotification () {
300
+ const parentSelector = isMobile() ? '.o-share--horizontal' : '.o-share--vertical';
301
+ const parentNode = document.querySelector(parentSelector);
302
+
303
+ // We're not supporting multiple notifications for now
304
+ // If a notification is present, we'll silently avoid showing another
305
+ if (document.querySelector('.myft-notification') || !parentNode) {
306
+ return;
307
+ }
308
+
309
+ const content = `
310
+ <p role="alert">Removed from <a href="https://www.ft.com/myft/saved-articles">saved articles</a> in myFT</p>
311
+ `;
312
+
313
+ const contentNode = stringToHTMLElement(content);
314
+
315
+ const container = document.createElement('div');
316
+ container.className = 'myft-notification';
317
+ container.appendChild(contentNode);
318
+
319
+ parentNode.appendChild(container);
320
+
321
+ setTimeout(
322
+ () => parentNode.removeChild(container),
323
+ 5 * 1000
324
+ );
211
325
  }
212
326
 
213
327
  export function init () {
@@ -65,6 +65,7 @@ function signedInEventListeners () {
65
65
  const resultData = event.detail.results && event.detail.results[0];
66
66
  const isPressed = !!event.detail.results;
67
67
  buttonStates.setStateOfButton(relationshipName, event.detail.subject, isPressed, undefined, resultData, true);
68
+
68
69
  tracking.custom({
69
70
  subjectType,
70
71
  action,
@@ -2,72 +2,66 @@ import Overlay from '@financial-times/o-overlay';
2
2
  import myFtClient from 'next-myft-client';
3
3
  import { uuid } from 'n-ui-foundations';
4
4
  import getToken from './lib/get-csrf-token';
5
+ import isMobile from './lib/is-mobile';
6
+ import stringToHTMLElement from './lib/convert-string-to-html-element';
5
7
 
6
8
  const csrfToken = getToken();
7
9
 
8
- let lists;
10
+ let lists = [];
11
+ let haveLoadedLists = false;
12
+ let createListOverlay;
9
13
 
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) {
14
+ export default async function openSaveArticleToListVariant (name, contentId) {
15
+ function createList (list, cb) {
20
16
  if(!list) {
21
- return;
17
+ if (!lists.length) attachDescription();
18
+ return contentElement.addEventListener('click', openFormHandler, { once: true });
22
19
  }
23
20
 
24
21
  myFtClient.add('user', null, 'created', 'list', uuid(), { name: list, token: csrfToken })
25
22
  .then(detail => {
26
23
  myFtClient.add('list', detail.subject, 'contained', 'content', contentId, { token: csrfToken }).then((createdList) => {
27
- lists.push({ name: list, uuid: createdList.actorId, checked: true });
24
+ lists.unshift({ name: list, uuid: createdList.actorId, checked: true });
28
25
  const listElement = ListsElement(lists, addToList, removeFromList);
29
26
  const overlayContent = document.querySelector('.o-overlay__content');
30
27
  overlayContent.insertAdjacentElement('afterbegin', listElement);
31
28
  const announceListContainer = document.querySelector('.myft-ui-create-list-variant-announcement');
32
29
  announceListContainer.textContent = `${list} created`;
33
- triggerCreateListEvent(contentId);
34
30
  contentElement.addEventListener('click', openFormHandler, { once: true });
31
+ cb(contentId, createdList.actorId);
35
32
  });
33
+ })
34
+ .catch(() => {
35
+ if (!lists.length) attachDescription();
36
+ return contentElement.addEventListener('click', openFormHandler, { once: true });
36
37
  });
37
38
  }
38
39
 
39
- function addToList (list) {
40
+ function addToList (list, cb) {
40
41
  if(!list) {
41
42
  return;
42
43
  }
43
44
 
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);
45
+ myFtClient.add('list', list.uuid, 'contained', 'content', contentId, { token: csrfToken }).then((addedList) => {
46
+ cb();
47
+ triggerAddToListEvent(contentId, addedList.actorId);
51
48
  });
52
49
  }
53
50
 
54
- function removeFromList (list) {
51
+ function removeFromList (list, cb) {
55
52
  if(!list) {
56
53
  return;
57
54
  }
58
55
 
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);
56
+ myFtClient.remove('list', list.uuid, 'contained', 'content', contentId, { token: csrfToken }).then((removedList) => {
57
+ cb();
58
+ triggerRemoveFromListEvent(contentId, removedList.actorId);
66
59
  });
67
60
  }
68
61
 
69
- if (!lists) {
62
+ if (!haveLoadedLists) {
70
63
  lists = await getLists(contentId);
64
+ haveLoadedLists = true;
71
65
  }
72
66
 
73
67
  const overlays = Overlay.getOverlays();
@@ -77,9 +71,9 @@ async function openSaveArticleToListVariant (name, contentId) {
77
71
  }
78
72
 
79
73
  const headingElement = HeadingElement();
80
- let [contentElement, removeDescription] = ContentElement(!lists.length);
74
+ let [contentElement, removeDescription, attachDescription] = ContentElement(!lists.length);
81
75
 
82
- const createListOverlay = new Overlay(name, {
76
+ createListOverlay = new Overlay(name, {
83
77
  html: contentElement,
84
78
  heading: { title: headingElement.outerHTML },
85
79
  modal: false,
@@ -87,57 +81,62 @@ async function openSaveArticleToListVariant (name, contentId) {
87
81
  class: 'myft-ui-create-list-variant',
88
82
  });
89
83
 
90
- const realignListener = realignOverlay(window.scrollY);
91
-
92
84
  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);
85
+ const overlayContent = document.querySelector('.o-overlay__content');
86
+ const overlayContainer = document.querySelector('.o-overlay');
87
+ // we don't want to close the overlay if the click happened inside the
88
+ // overlay, except if the click happened on the overlay close button
89
+ const isTargetInsideOverlay = overlayContainer.contains(e.target) && !e.target.classList.contains('o-overlay__close');
90
+ if(createListOverlay.visible && (!overlayContent || !isTargetInsideOverlay)) {
91
+ createListOverlay.close();
100
92
  }
101
93
  }
102
94
 
103
95
  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
- }
96
+ const formElement = FormElement(createList);
97
+ const overlayContent = document.querySelector('.o-overlay__content');
98
+ removeDescription();
99
+ overlayContent.insertAdjacentElement('beforeend', formElement);
100
+ formElement.elements[0].focus();
101
+ }
102
+
103
+ function getScrollHandler (target) {
104
+ return realignOverlay(window.scrollY, target);
105
+ }
106
+
107
+ function resizeHandler () {
108
+ positionOverlay(createListOverlay.wrapper);
113
109
  }
114
110
 
115
111
  createListOverlay.open();
116
- createListOverlay.wrapper.addEventListener('oOverlay.ready', (data) => {
117
- realignListener(data.currentTarget);
118
112
 
119
- if (lists && lists.length) {
113
+ const scrollHandler = getScrollHandler(createListOverlay.wrapper);
114
+
115
+ createListOverlay.wrapper.addEventListener('oOverlay.ready', (data) => {
116
+ if (lists.length) {
120
117
  const listElement = ListsElement(lists, addToList, removeFromList);
121
118
  const overlayContent = document.querySelector('.o-overlay__content');
122
119
  overlayContent.insertAdjacentElement('afterbegin', listElement);
123
120
  }
124
121
 
122
+ positionOverlay(data.currentTarget);
123
+
125
124
  contentElement.addEventListener('click', openFormHandler, { once: true });
126
125
 
127
- document.querySelector('.article-content').addEventListener('click', outsideClickHandler, { once: true });
128
- });
126
+ document.querySelector('.article-content').addEventListener('click', outsideClickHandler);
129
127
 
130
- window.addEventListener('scroll', realignListener(createListOverlay.wrapper, window.scrollY));
128
+ window.addEventListener('scroll', scrollHandler);
131
129
 
132
- window.addEventListener('oViewport.resize', () => {
133
- realignListener(createListOverlay.wrapper);
130
+ window.addEventListener('oViewport.resize', resizeHandler);
134
131
  });
135
- }
136
132
 
137
- function stringToHTMLElement (string) {
138
- const template = document.createElement('template');
139
- template.innerHTML = string.trim();
140
- return template.content.firstChild;
133
+ createListOverlay.wrapper.addEventListener('oOverlay.destroy', () => {
134
+ window.removeEventListener('scroll', scrollHandler);
135
+
136
+ window.removeEventListener('oViewport.resize', resizeHandler);
137
+
138
+ document.querySelector('.article-content').removeEventListener('click', outsideClickHandler);
139
+ });
141
140
  }
142
141
 
143
142
  function FormElement (createList) {
@@ -157,16 +156,16 @@ function FormElement (createList) {
157
156
  const formElement = stringToHTMLElement(formString);
158
157
 
159
158
  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
- }
159
+ event.preventDefault();
160
+ event.stopPropagation();
161
+ const inputListName = formElement.querySelector('input[name="list-name"]');
162
+ createList(inputListName.value, ((contentId, listId) => {
163
+ triggerCreateListEvent(contentId, listId);
164
+ triggerAddToListEvent(contentId, listId);
165
+ positionOverlay(createListOverlay.wrapper);
166
+ }));
167
+ inputListName.value = '';
168
+ formElement.remove();
170
169
  }
171
170
 
172
171
  formElement.querySelector('button[type="submit"]').addEventListener('click', handleSubmit);
@@ -174,12 +173,14 @@ function FormElement (createList) {
174
173
  return formElement;
175
174
  }
176
175
 
177
- function ContentElement (description) {
176
+ function ContentElement (hasDescription) {
177
+ const description = '<p class="myft-ui-create-list-variant-add-description">Lists are a simple way to curate your content</p>';
178
+
178
179
  const content = `
179
180
  <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>
181
+ <button class="myft-ui-create-list-variant-add" data-trackable="add-to-new-list" text="Add to a new list">Add to a new list</button>
182
+ ${hasDescription ? `
183
+ ${description}
183
184
  ` : ''}
184
185
  </div>
185
186
  `;
@@ -193,12 +194,17 @@ function ContentElement (description) {
193
194
  }
194
195
  }
195
196
 
196
- return [contentElement, removeDescription];
197
+ function attachDescription () {
198
+ const descriptionElement = stringToHTMLElement(description);
199
+ contentElement.insertAdjacentElement('beforeend', descriptionElement);
200
+ }
201
+
202
+ return [contentElement, removeDescription, attachDescription];
197
203
  }
198
204
 
199
205
  function HeadingElement () {
200
206
  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>
207
+ <span class="myft-ui-create-list-variant-heading">Added to <a href="https://www.ft.com/myft/saved-articles" data-trackable="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
208
  `;
203
209
 
204
210
  return stringToHTMLElement(heading);
@@ -230,8 +236,9 @@ function ListsElement (lists, addToList, removeFromList) {
230
236
 
231
237
  function ListCheckboxElement (addToList, removeFromList) {
232
238
  return function (list) {
239
+
233
240
  const listCheckbox = `<label>
234
- <input type="checkbox" name="default" value="${list.name}" ${list.checked ? 'checked' : ''}>
241
+ <input type="checkbox" name="default" value="${list.uuid}" ${list.checked ? 'checked' : ''}>
235
242
  <span class="o-forms-input__label">
236
243
  <span class="o-normalise-visually-hidden">
237
244
  ${list.checked ? 'Remove article from ' : 'Add article to ' }
@@ -247,7 +254,15 @@ function ListCheckboxElement (addToList, removeFromList) {
247
254
 
248
255
  function handleCheck (event) {
249
256
  event.preventDefault();
250
- return event.target.checked ? addToList(list) : removeFromList(list);
257
+ const isChecked = event.target.checked;
258
+
259
+ function onListUpdated () {
260
+ const indexToUpdate = lists.indexOf(list);
261
+ lists[indexToUpdate] = { ...lists[indexToUpdate], checked: isChecked };
262
+ listCheckboxElement.querySelector('input').checked = isChecked;
263
+ }
264
+
265
+ return isChecked ? addToList(list, onListUpdated) : removeFromList(list, onListUpdated);
251
266
  }
252
267
 
253
268
  input.addEventListener('click', handleCheck);
@@ -256,43 +271,47 @@ function ListCheckboxElement (addToList, removeFromList) {
256
271
  };
257
272
  }
258
273
 
259
- function realignOverlay (originalScrollPosition) {
260
- return function (target, currentScrollPosition) {
261
- try {
262
- if(currentScrollPosition && Math.abs(currentScrollPosition - originalScrollPosition) < 120) {
263
- return;
264
- }
274
+ function realignOverlay (originalScrollPosition, target) {
275
+ return function () {
276
+ const currentScrollPosition = window.scrollY;
265
277
 
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);
278
+ if(Math.abs(currentScrollPosition - originalScrollPosition) < 120) {
279
+ return;
280
+ }
281
+
282
+ if (currentScrollPosition) {
283
+ originalScrollPosition = currentScrollPosition;;
285
284
  }
285
+
286
+ positionOverlay(target);
286
287
  };
287
288
  }
288
289
 
289
- function isMobile () {
290
- const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
291
-
292
- return vw <= 980;
290
+ function positionOverlay (target) {
291
+ target.style['min-width'] = '340px';
292
+ target.style['width'] = '100%';
293
+ target.style['margin-top'] = '-50px';
294
+ target.style['left'] = 0;
295
+
296
+ if (isMobile()) {
297
+ const shareNavComponent = document.querySelector('.share-nav__horizontal');
298
+ const topHalfOffset = target.clientHeight + 10;
299
+ target.style['position'] = 'absolute';
300
+ target.style['margin-left'] = 0;
301
+ target.style['margin-top'] = 0;
302
+ target.style['top'] = calculateLargerScreenHalf(shareNavComponent) === 'ABOVE' ? `-${topHalfOffset}px` : '50px';
303
+ } else {
304
+ target.style['position'] = 'absolute';
305
+ target.style['margin-left'] = '45px';
306
+ target.style['top'] = '220px';
307
+ }
293
308
  }
294
309
 
295
310
  function calculateLargerScreenHalf (target) {
311
+ if (!target) {
312
+ return 'BELOW';
313
+ }
314
+
296
315
  const vh = Math.min(document.documentElement.clientHeight || 0, window.innerHeight || 0);
297
316
 
298
317
  const targetBox = target.getBoundingClientRect();
@@ -302,20 +321,21 @@ function calculateLargerScreenHalf (target) {
302
321
  return spaceBelow < spaceAbove ? 'ABOVE' : 'BELOW';
303
322
  }
304
323
 
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
- });
324
+ async function getLists (contentId) {
325
+ return myFtClient.getListsContent()
326
+ .then(results => results.items.map(list => {
327
+ const isChecked = Array.isArray(list.content) && list.content.some(content => content.uuid === contentId);
328
+ return { name: list.name, uuid: list.uuid, checked: isChecked, content: list.content };
329
+ }));
311
330
  }
312
331
 
313
- function triggerAddToListEvent (contentId) {
332
+ function triggerAddToListEvent (contentId, listId) {
314
333
  return document.body.dispatchEvent(new CustomEvent('oTracking.event', {
315
334
  detail: {
316
- category: 'professorLists',
317
- action: 'add-to-list',
335
+ category: 'list',
336
+ action: 'add-success',
318
337
  article_id: contentId,
338
+ list_id: listId,
319
339
  teamName: 'customer-products-us-growth',
320
340
  amplitudeExploratory: true
321
341
  },
@@ -323,12 +343,13 @@ function triggerAddToListEvent (contentId) {
323
343
  }));
324
344
  }
325
345
 
326
- function triggerRemoveFromListEvent (contentId) {
346
+ function triggerRemoveFromListEvent (contentId, listId) {
327
347
  return document.body.dispatchEvent(new CustomEvent('oTracking.event', {
328
348
  detail: {
329
- category: 'professorLists',
330
- action: 'remove-from-list',
349
+ category: 'list',
350
+ action: 'remove-success',
331
351
  article_id: contentId,
352
+ list_id: listId,
332
353
  teamName: 'customer-products-us-growth',
333
354
  amplitudeExploratory: true
334
355
  },
@@ -336,34 +357,16 @@ function triggerRemoveFromListEvent (contentId) {
336
357
  }));
337
358
  }
338
359
 
339
- function triggerCreateListEvent (contentId) {
360
+ function triggerCreateListEvent (contentId, listId) {
340
361
  document.body.dispatchEvent(new CustomEvent('oTracking.event', {
341
362
  detail: {
342
- category: 'professorLists',
343
- action: 'create-list',
363
+ category: 'list',
364
+ action: 'create-success',
344
365
  article_id: contentId,
366
+ list_id: listId,
345
367
  teamName: 'customer-products-us-growth',
346
368
  amplitudeExploratory: true
347
369
  },
348
370
  bubbles: true
349
371
  }));
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
372
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@financial-times/n-myft-ui",
3
- "version": "28.0.7",
3
+ "version": "28.2.1",
4
4
  "description": "Client side component for interaction with myft",
5
5
  "main": "server.js",
6
6
  "scripts": {
@@ -65,6 +65,7 @@
65
65
  "karma-sinon": "^1.0.5",
66
66
  "karma-sinon-chai": "2.0.2",
67
67
  "karma-sourcemap-loader": "^0.3.7",
68
+ "karma-viewport": "^1.0.9",
68
69
  "karma-webpack": "^4.0.2",
69
70
  "lintspaces-cli": "^0.7.0",
70
71
  "lolex": "5.1.1",
@@ -103,13 +104,13 @@
103
104
  "form-serialize": "^0.7.2",
104
105
  "ftdomdelegate": "^4.0.6",
105
106
  "js-cookie": "^2.2.1",
106
- "next-myft-client": "^10.1.0",
107
+ "next-myft-client": "^10.3.0",
107
108
  "next-session-client": "^4.0.0",
108
109
  "superstore-sync": "^2.1.1"
109
110
  },
110
111
  "volta": {
111
112
  "node": "16.14.2",
112
- "npm": "7.20.2"
113
+ "npm": "7.24.2"
113
114
  },
114
115
  "engines": {
115
116
  "node": "14.x || 16.x",
@@ -0,0 +1,20 @@
1
+ const expect = require('chai').expect;
2
+ const isMobile = require('../../myft/ui/lib/is-mobile');
3
+
4
+ /* global viewport */
5
+
6
+ describe('IsMobile', function () {
7
+ afterEach(() => {
8
+ viewport.reset();
9
+ });
10
+
11
+ it('detects a desktop device', () => {
12
+ viewport.set(981, 800);
13
+ expect(isMobile()).to.be.false;
14
+ });
15
+
16
+ it('detects a mobile device', () => {
17
+ viewport.set(979, 800);
18
+ expect(isMobile()).to.be.true;
19
+ });
20
+ });