@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.
- 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
|
}
|