@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.
- package/.circleci/config.yml +3 -0
- package/README.md +50 -0
- package/build-state/npm-shrinkwrap.json +14847 -15637
- package/components/collections/collections.jsx +68 -0
- package/components/collections/collections.test.js +83 -0
- package/components/concept-list/concept-list.jsx +69 -0
- package/components/concept-list/concept-list.test.js +116 -0
- package/components/csrf-token/input.jsx +1 -7
- package/components/csrf-token/{__tests__/input.test.js → input.test.js} +2 -2
- package/components/follow-button/follow-button.jsx +6 -4
- package/components/follow-button/{__tests__/follow-button.test.js → follow-button.test.js} +4 -4
- package/components/index.js +17 -0
- package/components/instant-alert/instant-alert.jsx +73 -0
- package/components/instant-alert/instant-alert.test.js +86 -0
- package/components/pin-button/pin-button.jsx +40 -0
- package/components/pin-button/pin-button.test.js +57 -0
- package/components/save-for-later/save-for-later.jsx +101 -0
- package/components/save-for-later/save-for-later.test.js +59 -0
- package/demos/app.js +2 -4
- package/demos/templates/demo.html +8 -9
- package/demos/templates/demo.jsx +93 -1
- package/dist/bundles/bundle.js +3232 -0
- package/jsx-migration.md +16 -0
- package/package.json +7 -3
- package/webpack.config.js +34 -0
- package/components/collections/collections.html +0 -77
- package/components/concept-list/concept-list.html +0 -28
- package/components/instant-alert/instant-alert.html +0 -47
- package/components/pin-button/pin-button.html +0 -20
- 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
|
-
{{
|
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
|
-
{{
|
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
|
-
{{
|
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
|
-
{{
|
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
|
-
{{
|
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
|
-
{{
|
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
|
-
{{
|
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
|
-
{{
|
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>
|
package/demos/templates/demo.jsx
CHANGED
@@ -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
|
}
|