@financial-times/n-myft-ui 24.0.1 → 25.0.0-beta.3
Sign up to get free protection for your applications and to get access to all the features.
- package/.circleci/config.yml +18 -18
- package/README.md +51 -0
- package/build-state/npm-shrinkwrap.json +36418 -6805
- package/components/collections/collections.jsx +68 -0
- package/components/collections/collections.test.js +83 -0
- package/components/concept-list/concept-list.jsx +63 -0
- package/components/concept-list/concept-list.test.js +116 -0
- package/components/follow-button/__tests__/follow-button.test.js +3 -3
- package/components/follow-button/follow-button.jsx +3 -3
- package/components/index.js +15 -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 +103 -0
- package/components/save-for-later/save-for-later.test.js +59 -0
- package/demos/app.js +2 -4
- package/demos/templates/demo.html +7 -7
- package/demos/templates/demo.jsx +93 -1
- package/dist/bundles/bundle.js +3141 -0
- package/package.json +18 -8
- package/webpack.config.js +34 -0
- package/components/collections/collections.html +0 -77
- package/components/concept-list/concept-list.html +0 -28
- package/components/pin-button/pin-button.html +0 -20
- package/components/save-for-later/save-for-later.html +0 -67
@@ -0,0 +1,68 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import CsrfToken from '../csrf-token/input';
|
3
|
+
import FollowButton from '../follow-button/follow-button';
|
4
|
+
|
5
|
+
export default function Collections ({ title, liteStyle, flags, collectionName, trackable, concepts = [] }) {
|
6
|
+
const getLiteStyleModifier = () => liteStyle ? 'lite' : 'regular';
|
7
|
+
let formProps = {
|
8
|
+
method: 'POST',
|
9
|
+
action: '#',
|
10
|
+
'data-myft-ui': 'follow',
|
11
|
+
'data-concept-id': concepts.map(concept => concept.id).join(',')
|
12
|
+
};
|
13
|
+
|
14
|
+
if (collectionName) {
|
15
|
+
formProps['data-myft-tracking'] = collectionName;
|
16
|
+
}
|
17
|
+
|
18
|
+
return (
|
19
|
+
<section
|
20
|
+
className={`collection collection--${getLiteStyleModifier()}`}
|
21
|
+
data-trackable={trackable ? trackable : 'collection'}>
|
22
|
+
<header className={`collection__header collection__header--${getLiteStyleModifier()}`}>
|
23
|
+
<h2 className={`collection__title collection__title--${getLiteStyleModifier()}`}>
|
24
|
+
{title}
|
25
|
+
</h2>
|
26
|
+
</header>
|
27
|
+
<ul className="collection__concepts">
|
28
|
+
{concepts && concepts.map((concept, index) =>
|
29
|
+
<li className="collection__concept" key={index}>
|
30
|
+
<FollowButton variant={liteStyle ? 'primary' : 'inverse'} buttonText={concept.name} flags={flags} collectionName={collectionName} />
|
31
|
+
</li>)
|
32
|
+
}
|
33
|
+
</ul>
|
34
|
+
|
35
|
+
<div className="collection__meta">
|
36
|
+
<form
|
37
|
+
{...formProps}
|
38
|
+
className="n-myft-ui n-myft-ui--follow n-ui-hide-core collection-follow-all">
|
39
|
+
<input
|
40
|
+
type="hidden"
|
41
|
+
name="directType"
|
42
|
+
value={concepts.map(concept => concept.directType).join(',')}
|
43
|
+
/>
|
44
|
+
<CsrfToken />
|
45
|
+
<input
|
46
|
+
type="hidden"
|
47
|
+
name="name"
|
48
|
+
value={concepts.map(concept => concept.name).join(',')}
|
49
|
+
/>
|
50
|
+
<button
|
51
|
+
type="submit"
|
52
|
+
aria-pressed="false"
|
53
|
+
className={`collection-follow-all__button collection-follow-all__button--${getLiteStyleModifier()}`}
|
54
|
+
data-trackable="follow all"
|
55
|
+
data-concept-id={concepts.map(concept => concept.id).join(',')}
|
56
|
+
aria-label={`Add all topics in the ${title} collection to my F T`}
|
57
|
+
data-alternate-label={`Remove all topics in the ${title} collection from my F T`}
|
58
|
+
data-alternate-text="Added"
|
59
|
+
title={`Add all topics in the ${title} collection to my F T`}>
|
60
|
+
Add all to myFT
|
61
|
+
</button>
|
62
|
+
</form>
|
63
|
+
</div>
|
64
|
+
</section>
|
65
|
+
|
66
|
+
)
|
67
|
+
|
68
|
+
}
|
@@ -0,0 +1,83 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import Collections from './collections';
|
3
|
+
import { render, screen } from '@testing-library/react';
|
4
|
+
import '@testing-library/jest-dom';
|
5
|
+
|
6
|
+
const fixtures = {
|
7
|
+
'title': 'European Union',
|
8
|
+
'concepts': [
|
9
|
+
{
|
10
|
+
'id': '00000000-0000-0000-0000-000000000000',
|
11
|
+
'prefLabel': 'EU immigration',
|
12
|
+
'directType': 'http://www.ft.com/ontology/Topic',
|
13
|
+
'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000000',
|
14
|
+
'name': 'EU immigration'
|
15
|
+
},
|
16
|
+
{
|
17
|
+
'id': '00000000-0000-0000-0000-000000000001',
|
18
|
+
'prefLabel': 'Europe Quantitative Easing',
|
19
|
+
'directType': 'http://www.ft.com/ontology/Topic',
|
20
|
+
'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000000',
|
21
|
+
'name': 'Europe Quantitative Easing'
|
22
|
+
},
|
23
|
+
{
|
24
|
+
'id': '00000000-0000-0000-0000-000000000002',
|
25
|
+
'prefLabel': 'EU financial regulation',
|
26
|
+
'directType': 'http://www.ft.com/ontology/Topic',
|
27
|
+
'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000000',
|
28
|
+
'name': 'EU financial regulation'
|
29
|
+
},
|
30
|
+
{
|
31
|
+
'id': '00000000-0000-0000-0000-000000000003',
|
32
|
+
'prefLabel': 'EU nothing',
|
33
|
+
'directType': 'http://www.ft.com/ontology/Topic',
|
34
|
+
'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000000',
|
35
|
+
'name': 'EU nothing'
|
36
|
+
},
|
37
|
+
{
|
38
|
+
'id': '00000000-0000-0000-0000-000000000004',
|
39
|
+
'prefLabel': 'EU trade',
|
40
|
+
'directType': 'http://www.ft.com/ontology/Topic',
|
41
|
+
'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000000',
|
42
|
+
'name': 'EU trade'
|
43
|
+
}
|
44
|
+
]
|
45
|
+
};
|
46
|
+
const joinedDirectTypes = 'http://www.ft.com/ontology/Topic,http://www.ft.com/ontology/Topic,http://www.ft.com/ontology/Topic,http://www.ft.com/ontology/Topic,http://www.ft.com/ontology/Topic';
|
47
|
+
|
48
|
+
const flags = {
|
49
|
+
myFtApi: true,
|
50
|
+
myFtApiWrite: true
|
51
|
+
};
|
52
|
+
|
53
|
+
describe('Concept List', () => {
|
54
|
+
|
55
|
+
test('It renders the title of the collection', async () => {
|
56
|
+
render(<Collections {...fixtures} flags={flags} />);
|
57
|
+
expect(await screen.findByText('European Union')).toBeTruthy();
|
58
|
+
});
|
59
|
+
|
60
|
+
test('It renders label for the concept button', async () => {
|
61
|
+
render(<Collections {...fixtures} flags={flags} />);
|
62
|
+
expect(await screen.findByText('EU immigration')).toBeTruthy();
|
63
|
+
expect(await screen.findByText('Europe Quantitative Easing')).toBeTruthy();
|
64
|
+
expect(await screen.findByText('EU financial regulation')).toBeTruthy();
|
65
|
+
expect(await screen.findByText('EU nothing')).toBeTruthy();
|
66
|
+
expect(await screen.findByText('EU trade')).toBeTruthy();
|
67
|
+
});
|
68
|
+
|
69
|
+
test('It renders form "Add all to my FT" from', () => {
|
70
|
+
const { container} = render(<Collections {...fixtures} flags={flags} />);
|
71
|
+
const formElement = container.querySelector('form[action="#"]');
|
72
|
+
expect(formElement).toBeTruthy();
|
73
|
+
expect(formElement.method).toEqual('post');
|
74
|
+
});
|
75
|
+
|
76
|
+
test('It renders directType input with value of types joined', () => {
|
77
|
+
const { container} = render(<Collections {...fixtures} flags={flags} />);
|
78
|
+
const directTypeElement = container.querySelector('input[name="directType"]');
|
79
|
+
expect(directTypeElement).toBeTruthy();
|
80
|
+
expect(directTypeElement.value).toEqual(joinedDirectTypes);
|
81
|
+
});
|
82
|
+
|
83
|
+
});
|
@@ -0,0 +1,63 @@
|
|
1
|
+
import React, { Fragment } from 'react';
|
2
|
+
import FollowButton from '../follow-button/follow-button';
|
3
|
+
|
4
|
+
export default function ConceptList ({ flags, concepts, contentType, conceptListTitle, trackable }) {
|
5
|
+
|
6
|
+
const {
|
7
|
+
myFtApi,
|
8
|
+
myFtApiWrite
|
9
|
+
} = flags;
|
10
|
+
|
11
|
+
const generateTrackableProps = (primary, seconday) => {
|
12
|
+
return {
|
13
|
+
'data-trackable': primary ? primary : seconday
|
14
|
+
}
|
15
|
+
}
|
16
|
+
|
17
|
+
const shouldDisplay = () => {
|
18
|
+
if(myFtApi && myFtApiWrite && Array.isArray(concepts) && concepts.length) {
|
19
|
+
return true
|
20
|
+
}
|
21
|
+
|
22
|
+
return false;
|
23
|
+
}
|
24
|
+
|
25
|
+
|
26
|
+
return (
|
27
|
+
|
28
|
+
<Fragment>
|
29
|
+
{shouldDisplay() &&
|
30
|
+
<div
|
31
|
+
className='concept-list'
|
32
|
+
{...generateTrackableProps(trackable, 'concept-list')}>
|
33
|
+
{
|
34
|
+
(contentType || conceptListTitle) &&
|
35
|
+
<h2 className='concept-list__title'>
|
36
|
+
{conceptListTitle ? conceptListTitle : `Follow the topics in this ${contentType}`}
|
37
|
+
</h2>
|
38
|
+
}
|
39
|
+
<ul className='concept-list__list'>
|
40
|
+
{concepts.map((concept, index) => {
|
41
|
+
const {
|
42
|
+
relativeUrl,
|
43
|
+
url,
|
44
|
+
conceptTrackable,
|
45
|
+
prefLabel,
|
46
|
+
id
|
47
|
+
} = concept;
|
48
|
+
return (
|
49
|
+
<li key={index} className='concept-list__list-item'>
|
50
|
+
<a
|
51
|
+
href={relativeUrl || url}
|
52
|
+
{...generateTrackableProps(conceptTrackable, 'concept')}
|
53
|
+
className='concept-list__concept'>
|
54
|
+
{prefLabel}
|
55
|
+
</a>
|
56
|
+
<FollowButton conceptId={id} name={prefLabel} flags={flags} />
|
57
|
+
</li>
|
58
|
+
)
|
59
|
+
})}
|
60
|
+
</ul>
|
61
|
+
</div>}
|
62
|
+
</Fragment>)
|
63
|
+
}
|
@@ -0,0 +1,116 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import ConceptList from './concept-list';
|
3
|
+
import { render, screen } from '@testing-library/react';
|
4
|
+
import '@testing-library/jest-dom';
|
5
|
+
|
6
|
+
const fixtures = [
|
7
|
+
{
|
8
|
+
'conceptListTitle': 'Follow european union things',
|
9
|
+
'concepts': [
|
10
|
+
{
|
11
|
+
'id': '00000000-0000-0000-0000-000000000161',
|
12
|
+
'prefLabel': 'EU immigration',
|
13
|
+
'directType': 'http://www.ft.com/ontology/Topic',
|
14
|
+
'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000161',
|
15
|
+
'name': 'EU immigration'
|
16
|
+
},
|
17
|
+
{
|
18
|
+
'id': '00000000-0000-0000-0000-000000000162',
|
19
|
+
'prefLabel': 'Europe Quantitative Easing',
|
20
|
+
'directType': 'http://www.ft.com/ontology/Topic',
|
21
|
+
'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000162',
|
22
|
+
'name': 'Europe Quantitative Easing'
|
23
|
+
},
|
24
|
+
{
|
25
|
+
'id': '00000000-0000-0000-0000-000000000163',
|
26
|
+
'prefLabel': 'EU financial regulation',
|
27
|
+
'directType': 'http://www.ft.com/ontology/Topic',
|
28
|
+
'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000163',
|
29
|
+
'name': 'EU financial regulation'
|
30
|
+
},
|
31
|
+
{
|
32
|
+
'id': '00000000-0000-0000-0000-000000000164',
|
33
|
+
'prefLabel': 'EU nothing',
|
34
|
+
'directType': 'http://www.ft.com/ontology/Topic',
|
35
|
+
'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000164',
|
36
|
+
'name': 'EU nothing'
|
37
|
+
},
|
38
|
+
{
|
39
|
+
'id': '00000000-0000-0000-0000-000000000165',
|
40
|
+
'prefLabel': 'EU trade',
|
41
|
+
'directType': 'http://www.ft.com/ontology/Topic',
|
42
|
+
'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000165',
|
43
|
+
'name': 'EU trade'
|
44
|
+
}
|
45
|
+
]
|
46
|
+
},
|
47
|
+
{
|
48
|
+
'contentType': 'search',
|
49
|
+
'concepts': [
|
50
|
+
{
|
51
|
+
'id': '00000000-0000-0000-0000-000000000166',
|
52
|
+
'prefLabel': 'Noodle',
|
53
|
+
'directType': 'http://www.ft.com/ontology/Topic',
|
54
|
+
'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000166',
|
55
|
+
'name': 'Noodle'
|
56
|
+
},
|
57
|
+
{
|
58
|
+
'id': '00000000-0000-0000-0000-000000000167',
|
59
|
+
'prefLabel': 'Green apples',
|
60
|
+
'directType': 'http://www.ft.com/ontology/Topic',
|
61
|
+
'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000167',
|
62
|
+
'name': 'Green apples'
|
63
|
+
},
|
64
|
+
{
|
65
|
+
'id': '00000000-0000-0000-0000-000000000168',
|
66
|
+
'prefLabel': 'Fox blood',
|
67
|
+
'directType': 'http://www.ft.com/ontology/Topic',
|
68
|
+
'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000168',
|
69
|
+
'name': 'Fox blood'
|
70
|
+
},
|
71
|
+
{
|
72
|
+
'id': '00000000-0000-0000-0000-000000000169',
|
73
|
+
'prefLabel': 'Dog party',
|
74
|
+
'directType': 'http://www.ft.com/ontology/Topic',
|
75
|
+
'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000169',
|
76
|
+
'name': 'Dog party'
|
77
|
+
},
|
78
|
+
{
|
79
|
+
'id': '00000000-0000-0000-0000-000000000170',
|
80
|
+
'prefLabel': 'Fifth thing',
|
81
|
+
'directType': 'http://www.ft.com/ontology/Topic',
|
82
|
+
'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000170',
|
83
|
+
'name': 'Fifth thing'
|
84
|
+
}
|
85
|
+
]
|
86
|
+
},
|
87
|
+
];
|
88
|
+
|
89
|
+
|
90
|
+
const flags = {
|
91
|
+
myFtApi: true,
|
92
|
+
myFtApiWrite: true
|
93
|
+
};
|
94
|
+
|
95
|
+
describe('Concept List', () => {
|
96
|
+
|
97
|
+
test('It renders conceptListTitle value as title when conceptListTitle is provided', async () => {
|
98
|
+
render(<ConceptList {...fixtures[0]} flags={flags} />);
|
99
|
+
expect(await screen.findByText('Follow european union things')).toBeTruthy();
|
100
|
+
});
|
101
|
+
|
102
|
+
test('It renders "Follow the topics in this {conceptType}" value as title when conceptType is provided', async () => {
|
103
|
+
render(<ConceptList {...fixtures[1]} flags={flags} />);
|
104
|
+
expect(await screen.findByText('Follow the topics in this search')).toBeTruthy();
|
105
|
+
});
|
106
|
+
|
107
|
+
test('It renders label for the concept button', async () => {
|
108
|
+
render(<ConceptList {...fixtures[0]} flags={flags} />);
|
109
|
+
expect(await screen.findByText('EU immigration')).toBeTruthy();
|
110
|
+
expect(await screen.findByText('Europe Quantitative Easing')).toBeTruthy();
|
111
|
+
expect(await screen.findByText('EU financial regulation')).toBeTruthy();
|
112
|
+
expect(await screen.findByText('EU nothing')).toBeTruthy();
|
113
|
+
expect(await screen.findByText('EU trade')).toBeTruthy();
|
114
|
+
});
|
115
|
+
|
116
|
+
});
|
@@ -16,7 +16,7 @@ describe('Follow button', () => {
|
|
16
16
|
|
17
17
|
test('It renders default button', async () => {
|
18
18
|
render(<FollowButton {...props} />);
|
19
|
-
expect(screen.findByText('Add to myFT')).toBeTruthy();
|
19
|
+
expect(await screen.findByText('Add to myFT')).toBeTruthy();
|
20
20
|
});
|
21
21
|
|
22
22
|
test('It renders a variant', async () => {
|
@@ -29,12 +29,12 @@ describe('Follow button', () => {
|
|
29
29
|
expect(container.querySelector(`form[action='/myft/add/${props.conceptId}']`)).toBeTruthy();
|
30
30
|
});
|
31
31
|
|
32
|
-
test('Button state changes when attributes change', () => {
|
32
|
+
test('Button state changes when attributes change', async () => {
|
33
33
|
render(<FollowButton {...props}
|
34
34
|
variant={'standard'}
|
35
35
|
setFollowButtonStateToSelected={true}
|
36
36
|
cacheablePersonalisedUrl={true} />);
|
37
|
-
expect(screen.findByText('Added')).toBeTruthy();
|
37
|
+
expect(await screen.findByText('Added')).toBeTruthy();
|
38
38
|
});
|
39
39
|
|
40
40
|
});
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import React from 'react';
|
1
|
+
import React, {Fragment} from 'react';
|
2
2
|
import CsrfToken from '../csrf-token/input';
|
3
3
|
|
4
4
|
function generateFormProps (props) {
|
@@ -148,7 +148,7 @@ export default function FollowButton (props) {
|
|
148
148
|
const getVariantClass = (variant) => variant ? `n-myft-follow-button--${variant}` : '';
|
149
149
|
|
150
150
|
return (
|
151
|
-
|
151
|
+
<Fragment>
|
152
152
|
{flags.myFtApiWrite && <form
|
153
153
|
className={`n-myft-ui n-myft-ui--follow ${extraClasses || ''}`}
|
154
154
|
method="GET"
|
@@ -168,7 +168,7 @@ export default function FollowButton (props) {
|
|
168
168
|
{getButtonText(props)}
|
169
169
|
</button>
|
170
170
|
</form>}
|
171
|
-
|
171
|
+
</Fragment>
|
172
172
|
);
|
173
173
|
|
174
174
|
}
|
@@ -0,0 +1,15 @@
|
|
1
|
+
import CsrfToken from './csrf-token/input';
|
2
|
+
import FollowButton from './follow-button/follow-button';
|
3
|
+
import ConceptList from './concept-list/concept-list';
|
4
|
+
import Collections from './collections/collections';
|
5
|
+
import SaveForLater from './save-for-later/save-for-later';
|
6
|
+
import PinButton from './pin-button/pin-button';
|
7
|
+
|
8
|
+
export {
|
9
|
+
CsrfToken,
|
10
|
+
FollowButton,
|
11
|
+
ConceptList,
|
12
|
+
Collections,
|
13
|
+
SaveForLater,
|
14
|
+
PinButton
|
15
|
+
};
|
@@ -0,0 +1,40 @@
|
|
1
|
+
import React, { Fragment } from 'react';
|
2
|
+
import CsrfToken from '../csrf-token/input';
|
3
|
+
export default function PinButton ({ showPrioritiseButton, id, name, directType, prioritised }) {
|
4
|
+
|
5
|
+
const getAction = () => `/__myft/api/core/prioritised/concept/${id}?method=${prioritised ? 'delete' : 'put'}`
|
6
|
+
|
7
|
+
return (
|
8
|
+
<Fragment>
|
9
|
+
{showPrioritiseButton &&
|
10
|
+
<Fragment>
|
11
|
+
<span className="myft-pin-divider"></span>
|
12
|
+
<div className="myft-pin-button-wrapper">
|
13
|
+
<form method="post" action={getAction()} data-myft-prioritise>
|
14
|
+
<CsrfToken />
|
15
|
+
<input type="hidden" value={name} name="name" />
|
16
|
+
<input type="hidden" value={directType || 'http://www.ft.com/ontology/concept/Concept'} name="directType" />
|
17
|
+
<div
|
18
|
+
className="n-myft-ui__announcement o-normalise-visually-hidden"
|
19
|
+
aria-live="assertive"
|
20
|
+
data-pressed-text={`${name} pinned in myFT.`}
|
21
|
+
data-unpressed-text={`Unpinned ${name} from myFT.`}
|
22
|
+
></div>
|
23
|
+
<button id={`myft-pin-button__${id}`}
|
24
|
+
className="myft-pin-button"
|
25
|
+
data-prioritise-button
|
26
|
+
data-trackable="prioritised"
|
27
|
+
data-concept-id={id}
|
28
|
+
data-prioritised={prioritised ? true : false}
|
29
|
+
aria-label={`${prioritised ? 'Unpin' : 'Pin'} ${name} ${prioritised ? 'from' : 'in'} myFT`}
|
30
|
+
aria-pressed={prioritised ? true : false}
|
31
|
+
title={`${prioritised ? 'Unpin' : 'Pin'} ${name}`}>
|
32
|
+
</button>
|
33
|
+
</form>
|
34
|
+
</div>
|
35
|
+
</Fragment>
|
36
|
+
}
|
37
|
+
</Fragment>
|
38
|
+
)
|
39
|
+
|
40
|
+
}
|
@@ -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
|
+
});
|
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
|
|