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

Sign up to get free protection for your applications and to get access to all the features.
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
  }