@financial-times/n-myft-ui 23.1.3 → 25.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. package/.circleci/config.yml +27 -30
  2. package/.circleci/shared-helpers/helper-npm-install-peer-deps +6 -5
  3. package/.github/settings.yml +1 -1
  4. package/.scss-lint.yml +3 -3
  5. package/Makefile +1 -0
  6. package/README.md +62 -8
  7. package/build-state/npm-shrinkwrap.json +49383 -17508
  8. package/components/collections/collections.jsx +68 -0
  9. package/components/collections/collections.test.js +83 -0
  10. package/components/concept-list/concept-list.jsx +55 -0
  11. package/components/concept-list/concept-list.test.js +116 -0
  12. package/components/csrf-token/__tests__/input.test.js +23 -0
  13. package/components/csrf-token/input.jsx +26 -0
  14. package/components/follow-button/__tests__/follow-button.test.js +40 -0
  15. package/components/follow-button/follow-button.jsx +174 -0
  16. package/components/index.js +15 -0
  17. package/components/instant-alert/instant-alert.html +1 -1
  18. package/components/pin-button/pin-button.jsx +40 -0
  19. package/components/pin-button/pin-button.test.js +57 -0
  20. package/components/save-for-later/save-for-later.jsx +103 -0
  21. package/components/save-for-later/save-for-later.test.js +59 -0
  22. package/components/unread-articles-indicator/date-fns.js +5 -12
  23. package/demos/app.js +39 -21
  24. package/demos/templates/demo-layout.html +1 -1
  25. package/demos/templates/demo.html +436 -415
  26. package/demos/templates/demo.jsx +125 -0
  27. package/dist/bundles/bundle.js +3133 -0
  28. package/jest.config.js +8 -0
  29. package/package.json +38 -13
  30. package/webpack.config.js +34 -0
  31. package/components/collections/collections.html +0 -85
  32. package/components/concept-list/concept-list.html +0 -31
  33. package/components/csrf-token/input.html +0 -5
  34. package/components/follow-button/follow-button.html +0 -79
  35. package/components/pin-button/pin-button.html +0 -20
  36. package/components/save-for-later/save-for-later.html +0 -67
  37. package/demos/fixtures/follow-button-plus-digest.json +0 -6
  38. package/demos/templates/digest-on-follow.html +0 -12
@@ -0,0 +1,57 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import '@testing-library/jest-dom';
4
+ import PinButton from './pin-button';
5
+
6
+ const flags = {
7
+ myFtApi: true,
8
+ myFtApiWrite: true
9
+ };
10
+
11
+ const fixtures = [
12
+ {
13
+ id: '00000000-0000-0000-0000-000000000022',
14
+ name: 'myFT Enterprises',
15
+ directType: 'http://www.ft.com/ontology/Topic',
16
+ showPrioritiseButton: true
17
+ },
18
+ {
19
+ id: '00000000-0000-0000-0000-000000000023',
20
+ name: 'myFT Enterprises',
21
+ directType: 'http://www.ft.com/ontology/Topic',
22
+ showPrioritiseButton: false
23
+ }
24
+ ];
25
+
26
+
27
+ describe('Pin Button', () => {
28
+
29
+ test('It renders', () => {
30
+ const { container } = render(<PinButton flags={flags} {...fixtures[0]} />);
31
+ expect(container.querySelector(`button[id="myft-pin-button__${fixtures[0].id}"]`)).toBeTruthy();
32
+ expect(container.querySelector('button[data-trackable="prioritised"]')).toBeTruthy();
33
+ });
34
+
35
+ test('It renders unprioritised', () => {
36
+ const { container } = render(<PinButton flags={flags} {...fixtures[0]} />);
37
+ expect(container.querySelector('button[aria-label="Pin myFT Enterprises in myFT"]')).toBeTruthy();
38
+ expect(container.querySelector('button[title="Pin myFT Enterprises"]')).toBeTruthy();
39
+ expect(container.querySelector('button[data-prioritised=false]')).toBeTruthy();
40
+ expect(container.querySelector(`button[data-concept-id="${fixtures[0].id}"]`)).toBeTruthy();
41
+ });
42
+
43
+ test('It renders with prioritised', () => {
44
+ const { container } = render(<PinButton flags={flags} prioritised={true} {...fixtures[0]} />);
45
+ expect(container.querySelector('button[aria-label="Unpin myFT Enterprises from myFT"]')).toBeTruthy();
46
+ expect(container.querySelector('button[title="Unpin myFT Enterprises"]')).toBeTruthy();
47
+ expect(container.querySelector('button[data-prioritised=true]')).toBeTruthy();
48
+ expect(container.querySelector(`button[data-concept-id="${fixtures[0].id}"]`)).toBeTruthy();
49
+ });
50
+
51
+ test('It renders the form element', () => {
52
+ const { container } = render(<PinButton flags={flags} {...fixtures[0]} />);
53
+ expect(container.querySelector('form[method="post"]')).toBeTruthy();
54
+ expect(container.querySelector(`form[action="/__myft/api/core/prioritised/concept/${fixtures[0].id}?method=put"]`)).toBeTruthy();
55
+ });
56
+
57
+ });
@@ -0,0 +1,103 @@
1
+ import React, { Fragment } from 'react';
2
+ import CsrfToken from '../csrf-token/input';
3
+
4
+ const ButtonContent = ({ saveButtonWithIcon, buttonText, isSaved, appIsStreamPage }) => {
5
+
6
+ return (<Fragment>
7
+ {
8
+ saveButtonWithIcon &&
9
+ <span className="save-button-with-icon-copy" data-variant-label>
10
+ {buttonText && buttonText}
11
+ {!buttonText && (isSaved ? 'Saved' : 'Save')}
12
+ </span>
13
+ }
14
+
15
+ {
16
+ !saveButtonWithIcon &&
17
+ <Fragment>
18
+ {buttonText && buttonText}
19
+ {!buttonText &&
20
+ <Fragment>
21
+ {
22
+ appIsStreamPage !== true &&
23
+ <Fragment>
24
+ <span className="save-button-longer-copy" data-variant-label>
25
+ {isSaved ? 'Saved ' : 'Save '}
26
+ </span>
27
+ <span className="n-myft-ui__button--viewport-large" aria-hidden="true">to myFT</span>
28
+ </Fragment>
29
+ }
30
+
31
+ {
32
+ appIsStreamPage === true && <span>{isSaved ? 'Saved' : 'Save'}</span>
33
+ }
34
+ </Fragment>
35
+ }
36
+ </Fragment>
37
+ }
38
+ </Fragment>);
39
+ }
40
+ export default function SaveForLater ({ flags, contentId, title, variant, trackableId, isSaved, appIsStreamPage, alternateText, saveButtonWithIcon, buttonText }) {
41
+
42
+ const { myFtApiWrite } = flags;
43
+
44
+ const generateSubmitButtonProps = () => {
45
+ let props = {
46
+ type: 'submit',
47
+ 'data-trackable': trackableId ? trackableId : 'save-for-later',
48
+ 'data-text-variant': appIsStreamPage !== true ? 'save-button-with-icon-copy' : 'save-button-longer-copy',
49
+ 'data-content-id': contentId,
50
+ className: saveButtonWithIcon ? 'n-myft-ui__save-button-with-icon' : `n-myft-ui__button ${variant ? `n-myft-ui__button--${variant}` : ''}`
51
+ };
52
+
53
+ if (isSaved) {
54
+ let titleText = `${title ? `${title} is` : ''} Saved to myFT`;
55
+ props['title'] = title;
56
+ props['aria-label'] = titleText;
57
+ props['data-alternate-label'] = title ? `Save ${title} to myFT for later` : 'Save this article to myFT for later';
58
+ props['aria-pressed'] = true;
59
+ } else {
60
+ let titleText = title ? `Save ${title} to myFT for later` : 'Save this article to myFT for later';
61
+ props['title'] = titleText;
62
+ props['aria-label'] = titleText;
63
+ props['data-alternate-label'] = `${title ? `${title} is` : ''} Saved to myFT`;
64
+ props['aria-pressed'] = false;
65
+ }
66
+
67
+ if (alternateText) {
68
+ props['data-alternate-text'] = alternateText;
69
+ } else if (isSaved) {
70
+ props['data-alternate-text'] = 'Save ';
71
+ } else {
72
+ props['data-alternate-text'] = 'Saved ';
73
+ }
74
+
75
+ return props;
76
+ }
77
+
78
+
79
+ return (
80
+ <Fragment>
81
+ {myFtApiWrite &&
82
+ <form className="n-myft-ui n-myft-ui--save" method="GET"
83
+ data-content-id={contentId}
84
+ data-myft-ui="saved"
85
+ action={`/myft/save/${contentId}`}
86
+ data-js-action={`/__myft/api/core/saved/content/${contentId}?method=put`}>
87
+ <CsrfToken />
88
+
89
+ <div
90
+ className="n-myft-ui__announcement o-normalise-visually-hidden"
91
+ aria-live="assertive"
92
+ data-pressed-text="Article saved in My FT."
93
+ data-unpressed-text="Removed article from My FT."
94
+ ></div>
95
+ <button {...generateSubmitButtonProps()}>
96
+ <ButtonContent buttonText={buttonText} saveButtonWithIcon={saveButtonWithIcon} isSaved={isSaved} appIsStreamPage={appIsStreamPage}/>
97
+ </button>
98
+ </form>
99
+ }
100
+ </Fragment>
101
+
102
+ )
103
+ }
@@ -0,0 +1,59 @@
1
+ import React from 'react';
2
+ import SaveForLater from './save-for-later';
3
+ import { render, screen } from '@testing-library/react';
4
+ import '@testing-library/jest-dom';
5
+
6
+ const flags = {
7
+ myFtApi: true,
8
+ myFtApiWrite: true
9
+ };
10
+
11
+ const fixture = {
12
+ contentId: '00000000-0000-0000-0000-000000000033',
13
+ title: 'myFT Enterprises'
14
+ };
15
+
16
+ describe('SaveForLater component', () => {
17
+
18
+ test('It renders', async () => {
19
+ render(<SaveForLater flags={flags} {...fixture}/>);
20
+ expect(await screen.findByText('Save')).toBeTruthy();
21
+ });
22
+
23
+ test('It renders button text wen provided', async () => {
24
+ render(<SaveForLater flags={flags} {...fixture} buttonText={'Globetrotter'}/>);
25
+ expect(await screen.findByText('Globetrotter')).toBeTruthy();
26
+ });
27
+
28
+ test('It renders the correct form action attribute', () => {
29
+ const { container } = render(<SaveForLater flags={flags} {...fixture} buttonText={'Globetrotter'}/>);
30
+ const formElement = container.querySelector(`form[action="/myft/save/${fixture.contentId}"]`);
31
+ expect(formElement).toBeTruthy();
32
+ });
33
+
34
+ test('It renders the correct form data-js-action attribute', () => {
35
+ const { container } = render(<SaveForLater flags={flags} {...fixture} buttonText={'Globetrotter'}/>);
36
+ const formElement = container.querySelector(`form[data-js-action="/__myft/api/core/saved/content/${fixture.contentId}?method=put"]`);
37
+ expect(formElement).toBeTruthy();
38
+ });
39
+
40
+ test('It renders the correct form method attribute', () => {
41
+ const { container } = render(<SaveForLater flags={flags} {...fixture} buttonText={'Globetrotter'}/>);
42
+ const formElement = container.querySelector('form[method="GET"]');
43
+ expect(formElement).toBeTruthy();
44
+ });
45
+
46
+ test('It renders the correct button data-text-variant attribute when appIsStreamPage=true', () => {
47
+ const { container } = render(<SaveForLater appIsStreamPage={true} flags={flags} {...fixture} buttonText={'Globetrotter'}/>);
48
+ const buttonElement = container.querySelector('button[data-text-variant="save-button-longer-copy"]');
49
+ expect(buttonElement).toBeTruthy();
50
+ });
51
+
52
+ describe('Saved', () => {
53
+ test('It renders saved item', async () => {
54
+ render(<SaveForLater isSaved={true} flags={flags} {...fixture}/>);
55
+ expect(await screen.findByText('Saved')).toBeTruthy();
56
+ });
57
+ });
58
+
59
+ });
@@ -1,5 +1,5 @@
1
1
  // date-fns from v2 doesn't accept String arguments anymore.
2
- // the detail => https://github.com/date-fns/date-fns/blob/master/CHANGELOG.md#200---2019-08-20
2
+ // the detail => https://github.com/date-fns/date-fns/blob/HEAD/CHANGELOG.md#200---2019-08-20
3
3
  // By adding validation for dates before their functions allows us to know it when unexpected value passed.
4
4
 
5
5
  import isTodayOriginal from 'date-fns/src/isToday';
@@ -11,22 +11,15 @@ import parseISO from 'date-fns/src/parseISO';
11
11
 
12
12
  const isValid = (date) => {
13
13
  if (!isValidOriginal(date)) {
14
- console.error('Invalid date passed', [date]); //eslint-disable-line
14
+ console.error("Invalid date passed", [date]); //eslint-disable-line
15
15
  }
16
16
  return date;
17
17
  };
18
18
 
19
19
  const isToday = (date) => isTodayOriginal(isValid(date));
20
- const isAfter = (date, dateToCompare) => isAfterOriginal(isValid(date), isValid(dateToCompare));
20
+ const isAfter = (date, dateToCompare) =>
21
+ isAfterOriginal(isValid(date), isValid(dateToCompare));
21
22
  const addMinutes = (date, amount) => addMinutesOriginal(isValid(date), amount);
22
23
  const startOfDay = (date) => startOfDayOriginal(isValid(date));
23
24
 
24
-
25
- export {
26
- isToday,
27
- isAfter,
28
- addMinutes,
29
- startOfDay,
30
- isValid,
31
- parseISO,
32
- };
25
+ export { isToday, isAfter, addMinutes, startOfDay, isValid, parseISO };
package/demos/app.js CHANGED
@@ -1,9 +1,16 @@
1
- const express = require('@financial-times/n-internal-tool');
1
+ require('sucrase/register');
2
+ const nExpress = require('@financial-times/n-express');
2
3
  const chalk = require('chalk');
3
4
  const errorHighlight = chalk.bold.red;
4
5
  const highlight = chalk.bold.green;
6
+ const { PageKitReactJSX } = require('@financial-times/dotcom-server-react-jsx');
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const handlebars = require('handlebars');
10
+ const { PageKitHandlebars, helpers } = require('@financial-times/dotcom-server-handlebars');
5
11
 
6
- const xHandlebars = require('@financial-times/x-handlebars');
12
+ const demoJSX = require('./templates/demo').default;
13
+ const demoLayoutSource = fs.readFileSync(path.join(__dirname, './templates/demo-layout.html'),'utf8').toString();
7
14
 
8
15
  const fixtures = {
9
16
  followButton: require('./fixtures/follow-button'),
@@ -14,48 +21,59 @@ const fixtures = {
14
21
  instantAlert: require('./fixtures/instant-alert')
15
22
  };
16
23
 
17
- const app = module.exports = express({
24
+ const app = module.exports = nExpress({
18
25
  name: 'public',
19
26
  systemCode: 'n-myft-ui-demo',
20
27
  withFlags: true,
21
- withHandlebars: true,
22
- withNavigation: false,
28
+ withConsent: false,
29
+ withServiceMetrics: false,
23
30
  withAnonMiddleware: false,
24
31
  hasHeadCss: false,
25
- layoutsDir: 'demos/templates',
26
- viewsDirectory: '/demos/templates',
27
32
  partialsDirectory: process.cwd(),
28
33
  directory: process.cwd(),
29
34
  demo: true,
30
- s3o: false,
31
- helpers: {
32
- x: xHandlebars()
33
- }
35
+ withBackendAuthentication: false,
34
36
  });
35
37
 
38
+ app.set('views', path.join(__dirname, '/templates'));
39
+ app.set('view engine', '.html');
40
+
41
+ app.engine('.html', new PageKitHandlebars({
42
+ cache: false,
43
+ handlebars,
44
+ helpers
45
+ }).engine);
46
+
47
+ app.use('/public', nExpress.static(path.join(__dirname, '../public'), { redirect: false }));
48
+
49
+ const jsxRenderer = (new PageKitReactJSX({ includeDoctype: false }));
50
+
36
51
  app.get('/', (req, res) => {
37
52
  res.render('demo', Object.assign({
38
53
  title: 'n-myft-ui demo',
39
- layout: 'demo-layout',
40
54
  flags: {
41
55
  myFtApi: true,
42
56
  myFtApiWrite: true
43
- }
57
+ },
44
58
  }, fixtures));
45
59
  });
46
60
 
47
- app.get('/digest-on-follow', (req, res) => {
48
- res.render('digest-on-follow', Object.assign({
49
- title: 'n-myft-ui digest on follow',
50
- layout: 'demo-layout',
61
+ app.get('/demo-jsx', async (req, res) => {
62
+ let demo = await jsxRenderer.render(demoJSX, Object.assign({
63
+ title: 'n-myft-ui demo',
51
64
  flags: {
52
65
  myFtApi: true,
53
- myFtApiWrite: true,
54
- },
55
- appIsStreamPage: false
56
- }, fixtures.followButtonPlusDigest));
66
+ myFtApiWrite: true
67
+ }
68
+ }, fixtures));
69
+
70
+ let template = handlebars.compile(demoLayoutSource);
71
+ let result = template({body: demo});
72
+
73
+ res.send(result);
57
74
  });
58
75
 
76
+
59
77
  function runPa11yTests () {
60
78
  const spawn = require('child_process').spawn;
61
79
  const pa11y = spawn('pa11y-ci');
@@ -14,7 +14,7 @@
14
14
  <div class="o-grid-row">
15
15
  <ul>
16
16
  <li><a href="/">Basic</a></li>
17
- <li><a href="/digest-on-follow">Follow plus digest button</a></li>
17
+ <li><a href="/demo-jsx">JSX demo</a></li>
18
18
  </ul>
19
19
  </div>
20
20
  </div>