@financial-times/n-myft-ui 25.0.1 → 26.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/.circleci/config.yml +3 -0
  2. package/README.md +50 -0
  3. package/build-state/npm-shrinkwrap.json +14847 -15637
  4. package/components/collections/collections.jsx +68 -0
  5. package/components/collections/collections.test.js +83 -0
  6. package/components/concept-list/concept-list.jsx +69 -0
  7. package/components/concept-list/concept-list.test.js +116 -0
  8. package/components/csrf-token/input.jsx +1 -7
  9. package/components/csrf-token/{__tests__/input.test.js → input.test.js} +2 -2
  10. package/components/follow-button/follow-button.jsx +6 -4
  11. package/components/follow-button/{__tests__/follow-button.test.js → follow-button.test.js} +4 -4
  12. package/components/index.js +17 -0
  13. package/components/instant-alert/instant-alert.jsx +73 -0
  14. package/components/instant-alert/instant-alert.test.js +86 -0
  15. package/components/pin-button/pin-button.jsx +40 -0
  16. package/components/pin-button/pin-button.test.js +57 -0
  17. package/components/save-for-later/save-for-later.jsx +101 -0
  18. package/components/save-for-later/save-for-later.test.js +59 -0
  19. package/demos/app.js +2 -4
  20. package/demos/templates/demo.html +8 -9
  21. package/demos/templates/demo.jsx +93 -1
  22. package/dist/bundles/bundle.js +3232 -0
  23. package/jsx-migration.md +16 -0
  24. package/package.json +7 -3
  25. package/webpack.config.js +34 -0
  26. package/components/collections/collections.html +0 -77
  27. package/components/concept-list/concept-list.html +0 -28
  28. package/components/instant-alert/instant-alert.html +0 -47
  29. package/components/pin-button/pin-button.html +0 -20
  30. package/components/save-for-later/save-for-later.html +0 -67
@@ -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 my F T"]')).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 my F T"]')).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,101 @@
1
+ import React, { Fragment } from 'react';
2
+ import CsrfToken from '../csrf-token/input';
3
+
4
+ const ButtonContent = ({ saveButtonWithIcon, buttonText, isSaved, appIsStreamPage }) => {
5
+
6
+ const DefaultButtonText = () => {
7
+ if (appIsStreamPage !== true) {
8
+ return <Fragment>
9
+ <span className="save-button-longer-copy" data-variant-label>
10
+ {isSaved ? 'Saved ' : 'Save '}
11
+ </span>
12
+ <span className="n-myft-ui__button--viewport-large" aria-hidden="true">to myFT</span>
13
+ </Fragment>
14
+ }
15
+
16
+ return <span>{isSaved ? 'Saved' : 'Save'}</span>;
17
+ }
18
+
19
+ return (<Fragment>
20
+ {
21
+ saveButtonWithIcon &&
22
+ <span className="save-button-with-icon-copy" data-variant-label>
23
+ {buttonText && buttonText}
24
+ {!buttonText && (isSaved ? 'Saved' : 'Save')}
25
+ </span>
26
+ }
27
+
28
+ {
29
+ !saveButtonWithIcon &&
30
+ <Fragment>
31
+ {buttonText && buttonText}
32
+ {!buttonText && <DefaultButtonText />
33
+ }
34
+ </Fragment>
35
+ }
36
+ </Fragment>);
37
+ }
38
+ export default function SaveForLater({ flags, contentId, title, variant, trackableId, isSaved, appIsStreamPage, alternateText, saveButtonWithIcon, buttonText, csrfToken, cacheablePersonalisedUrl }) {
39
+
40
+ const { myFtApiWrite } = flags;
41
+
42
+ const generateSubmitButtonProps = () => {
43
+ let props = {
44
+ type: 'submit',
45
+ 'data-trackable': trackableId ? trackableId : 'save-for-later',
46
+ 'data-text-variant': appIsStreamPage !== true ? 'save-button-with-icon-copy' : 'save-button-longer-copy',
47
+ 'data-content-id': contentId,
48
+ className: saveButtonWithIcon ? 'n-myft-ui__save-button-with-icon' : `n-myft-ui__button ${variant ? `n-myft-ui__button--${variant}` : ''}`
49
+ };
50
+
51
+ if (isSaved) {
52
+ let titleText = `${title ? `${title} is` : ''} saved to myFT`;
53
+ props['title'] = title;
54
+ props['aria-label'] = titleText;
55
+ props['data-alternate-label'] = title ? `Save ${title} to myFT for later` : 'Save this article to myFT for later';
56
+ props['aria-pressed'] = true;
57
+ } else {
58
+ let titleText = title ? `Save ${title} to myFT for later` : 'Save this article to myFT for later';
59
+ props['title'] = titleText;
60
+ props['aria-label'] = titleText;
61
+ props['data-alternate-label'] = `${title ? `${title} is` : ''} saved to myFT`;
62
+ props['aria-pressed'] = false;
63
+ }
64
+
65
+ if (alternateText) {
66
+ props['data-alternate-text'] = alternateText;
67
+ } else if (isSaved) {
68
+ props['data-alternate-text'] = 'Save ';
69
+ } else {
70
+ props['data-alternate-text'] = 'Saved ';
71
+ }
72
+
73
+ return props;
74
+ }
75
+
76
+
77
+ return (
78
+ <Fragment>
79
+ {myFtApiWrite &&
80
+ <form className="n-myft-ui n-myft-ui--save" method="GET"
81
+ data-content-id={contentId}
82
+ data-myft-ui="saved"
83
+ action={`/myft/save/${contentId}`}
84
+ data-js-action={`/__myft/api/core/saved/content/${contentId}?method=put`}>
85
+ <CsrfToken csrfToken={csrfToken} cacheablePersonalisedUrl={cacheablePersonalisedUrl} />
86
+
87
+ <div
88
+ className="n-myft-ui__announcement o-normalise-visually-hidden"
89
+ aria-live="assertive"
90
+ data-pressed-text="Article saved in My FT."
91
+ data-unpressed-text="Removed article from My FT."
92
+ ></div>
93
+ <button {...generateSubmitButtonProps()}>
94
+ <ButtonContent buttonText={buttonText} saveButtonWithIcon={saveButtonWithIcon} isSaved={isSaved} appIsStreamPage={appIsStreamPage} />
95
+ </button>
96
+ </form>
97
+ }
98
+ </Fragment>
99
+
100
+ )
101
+ }
@@ -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
+ });
package/demos/app.js CHANGED
@@ -41,9 +41,7 @@ app.set('view engine', '.html');
41
41
  app.engine('.html', new PageKitHandlebars({
42
42
  cache: false,
43
43
  handlebars,
44
- helpers: {
45
- ...helpers
46
- }
44
+ helpers
47
45
  }).engine);
48
46
 
49
47
  app.use('/public', nExpress.static(path.join(__dirname, '../public'), { redirect: false }));
@@ -56,7 +54,7 @@ app.get('/', (req, res) => {
56
54
  flags: {
57
55
  myFtApi: true,
58
56
  myFtApiWrite: true
59
- }
57
+ },
60
58
  }, fixtures));
61
59
  });
62
60
 
@@ -48,45 +48,44 @@
48
48
  <h2 class="demo-section__title">
49
49
  Save button
50
50
  </h2>
51
- {{> n-myft-ui/components/save-for-later/save-for-later }}
51
+ {{{renderReactComponent localPath="components/save-for-later/save-for-later" flags=@root.flags title=title contentId=contentId }}}
52
52
  {{/saveButton}}
53
53
 
54
54
  {{#saveButton}}
55
55
  <h2 class="demo-section__title">
56
56
  Unsave button
57
57
  </h2>
58
- {{> n-myft-ui/components/save-for-later/save-for-later isSaved=true }}
58
+ {{{renderReactComponent localPath="components/save-for-later/save-for-later" flags=@root.flags title=title contentId=contentId isSaved=true }}}
59
59
  {{/saveButton}}
60
60
 
61
61
  {{#saveButton}}
62
62
  <h2 class="demo-section__title">
63
63
  Save button with icon
64
64
  </h2>
65
- {{> n-myft-ui/components/save-for-later/save-for-later saveButtonWithIcon=true }}
65
+ {{{renderReactComponent localPath="components/save-for-later/save-for-later" flags=@root.flags title=title contentId=contentId saveButtonWithIcon=true }}}
66
66
  {{/saveButton}}
67
67
 
68
68
  {{#saveButton}}
69
69
  <h2 class="demo-section__title">
70
70
  Unsave button with icon
71
71
  </h2>
72
- {{> n-myft-ui/components/save-for-later/save-for-later isSaved=true saveButtonWithIcon=true }}
72
+ {{{renderReactComponent localPath="components/save-for-later/save-for-later" flags=@root.flags title=title contentId=contentId saveButtonWithIcon=true isSaved=true }}}
73
73
  {{/saveButton}}
74
74
 
75
75
  <h2 class="demo-section__title">
76
76
  Pin button
77
77
  </h2>
78
78
  {{#each pinButton}}
79
- {{> n-myft-ui/components/pin-button/pin-button }}
79
+ {{{renderReactComponent localPath="components/pin-button/pin-button" flags=@root.flags title=title id=id name=name directType=directType showPrioritiseButton=showPrioritiseButton }}}
80
80
  {{/each}}
81
81
 
82
82
  {{#instantAlert}}
83
83
  <h2 class="demo-section__title">
84
84
  Instant Alert
85
85
  </h2>
86
- {{> n-myft-ui/components/instant-alert/instant-alert }}
86
+ {{{renderReactComponent localPath="components/instant-alert/instant-alert" flags=@root.flags title=title conceptId=id name=name directType=directType }}}
87
87
  {{/instantAlert}}
88
88
 
89
-
90
89
  </div>
91
90
  </div>
92
91
  </section>
@@ -449,7 +448,7 @@
449
448
 
450
449
  {{#collections}}
451
450
  <div data-o-grid-colspan="3">
452
- {{> n-myft-ui/components/collections/collections }}
451
+ {{{renderReactComponent localPath="components/collections/collections" flags=@root.flags concepts=this.concepts title=this.title liteStyle=this.liteStyle collectionName=this.collectionName trackable=this.trackable}}}
453
452
  </div>
454
453
  {{/collections}}
455
454
  </div>
@@ -471,7 +470,7 @@
471
470
 
472
471
  {{#each conceptList}}
473
472
  <div data-o-grid-colspan="3">
474
- {{> n-myft-ui/components/concept-list/concept-list }}
473
+ {{{renderReactComponent localPath="components/concept-list/concept-list" flags=@root.flags concepts=this.concepts contentType=this.contentType conceptListTitle=this.conceptListTitle trackable=this.trackable}}}
475
474
  </div>
476
475
  {{/each}}
477
476
  </div>
@@ -1,5 +1,9 @@
1
1
  import React from 'react';
2
2
  import FollowButton from '../../components/follow-button/follow-button';
3
+ import ConceptList from '../../components/concept-list/concept-list';
4
+ import Collections from '../../components/collections/collections';
5
+ import { SaveForLater } from '../../components';
6
+ import { PinButton } from '../../components';
3
7
 
4
8
  export default function Demo (props) {
5
9
 
@@ -7,9 +11,13 @@ export default function Demo (props) {
7
11
  title,
8
12
  flags,
9
13
  followButton,
14
+ conceptList,
15
+ collections,
16
+ saveButton,
17
+ pinButton
10
18
  } = props;
11
19
 
12
- const followButtonProps = {...followButton, flags};
20
+ const followButtonProps = { ...followButton, flags };
13
21
 
14
22
  return (
15
23
  <div className="o-grid-container o-grid-container--snappy demo-container">
@@ -25,9 +33,93 @@ export default function Demo (props) {
25
33
  Follow button
26
34
  </h2>
27
35
  <FollowButton {...followButtonProps} />
36
+
37
+
38
+ <h2
39
+ className="demo-section__title">
40
+ x-dash follow button
41
+ </h2>
42
+
43
+ <FollowButton {...followButtonProps} buttonText={followButton.name} />
44
+
45
+
46
+ <h2 className="demo-section__title">
47
+ Save button
48
+ </h2>
49
+ <SaveForLater flags={flags} {...saveButton} />
50
+
51
+ <h2 className="demo-section__title">
52
+ Unsave button
53
+ </h2>
54
+ <SaveForLater flags={flags} {...saveButton} isSaved={true} />
55
+
56
+ <h2 className="demo-section__title">
57
+ Unsave button with icon
58
+ </h2>
59
+ <SaveForLater flags={flags} {...saveButton} saveButtonWithIcon={true} />
60
+
61
+ <h2 className="demo-section__title">
62
+ Save button with icon
63
+ </h2>
64
+ <SaveForLater flags={flags} {...saveButton} isSaved={true} saveButtonWithIcon={true} />
65
+
66
+ <h2 className="demo-section__title">
67
+ Pin button
68
+ </h2>
69
+
70
+ {pinButton.map((item, index) => <PinButton key={index} {...item}/>)}
71
+
72
+ </div>
73
+ </div>
74
+ </section>
75
+
76
+ <section
77
+ id="topic-list"
78
+ className="demo-section">
79
+ <div className="o-grid-row">
80
+ <div data-o-grid-colspan="12">
81
+ <h2 className="demo-section__title">
82
+ Topic list
83
+ </h2>
84
+
85
+ <p className="demo-section__description">
86
+ A list of topics to follow
87
+ </p>
88
+ </div>
89
+
90
+ {
91
+ conceptList && conceptList.map((list, index) =>
92
+ <div key={index} data-o-grid-colspan="3">
93
+ <ConceptList {...list} flags={flags} />
94
+ </div>)
95
+ }
96
+
97
+ </div>
98
+ </section>
99
+
100
+ <section
101
+ id="collections"
102
+ className="demo-section">
103
+ <div className="o-grid-row">
104
+ <div data-o-grid-colspan="12">
105
+ <h2 className="demo-section__title">
106
+ Collections
107
+ </h2>
108
+
109
+ <p className="demo-section__description">
110
+ Curated collections of topics to follow.
111
+ </p>
28
112
  </div>
113
+
114
+ {collections.map((collection, index) => (
115
+ <div key={index} data-o-grid-colspan="3">
116
+ <Collections {...collection} flags={flags} />
117
+ </div>
118
+ ))}
119
+
29
120
  </div>
30
121
  </section>
122
+
31
123
  </div>
32
124
  )
33
125
  }