@financial-times/n-myft-ui 26.0.0 → 27.2.0
Sign up to get free protection for your applications and to get access to all the features.
- package/.circleci/config.yml +42 -16
- package/.nvmrc +1 -1
- package/Makefile +0 -1
- package/README.md +2 -48
- package/build-state/npm-shrinkwrap.json +10147 -20187
- package/components/collections/collections.html +85 -0
- package/components/concept-list/concept-list.html +31 -0
- package/components/csrf-token/input.html +5 -0
- package/components/follow-button/follow-button.html +79 -0
- package/components/instant-alert/instant-alert.html +47 -0
- package/components/pin-button/pin-button.html +20 -0
- package/components/save-for-later/save-for-later.html +68 -0
- package/components/unread-articles-indicator/date-fns.js +6 -6
- package/demos/app.js +3 -26
- package/demos/templates/demo.html +11 -10
- package/myft/main.scss +145 -0
- package/myft/ui/lists.js +22 -0
- package/myft/ui/save-article-to-list-variant.js +376 -0
- package/package.json +16 -30
- package/components/collections/collections.jsx +0 -68
- package/components/collections/collections.test.js +0 -83
- package/components/concept-list/concept-list.jsx +0 -69
- package/components/concept-list/concept-list.test.js +0 -116
- package/components/csrf-token/input.jsx +0 -20
- package/components/csrf-token/input.test.js +0 -23
- package/components/follow-button/follow-button.jsx +0 -176
- package/components/follow-button/follow-button.test.js +0 -40
- package/components/index.js +0 -17
- package/components/instant-alert/instant-alert.jsx +0 -73
- package/components/instant-alert/instant-alert.test.js +0 -86
- package/components/pin-button/pin-button.jsx +0 -40
- package/components/pin-button/pin-button.test.js +0 -57
- package/components/save-for-later/save-for-later.jsx +0 -101
- package/components/save-for-later/save-for-later.test.js +0 -59
- package/demos/templates/demo-layout.html +0 -25
- package/demos/templates/demo.jsx +0 -125
- package/dist/bundles/bundle.js +0 -3232
- package/jest.config.js +0 -8
- package/jsx-migration.md +0 -16
- package/webpack.config.js +0 -34
@@ -1,69 +0,0 @@
|
|
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, csrfToken, cacheablePersonalisedUrl }) {
|
5
|
-
|
6
|
-
const {
|
7
|
-
myFtApi,
|
8
|
-
myFtApiWrite
|
9
|
-
} = flags;
|
10
|
-
|
11
|
-
const generateTrackableProps = (primary, secondary) => {
|
12
|
-
return {
|
13
|
-
'data-trackable': primary ? primary : secondary
|
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
|
-
{/* The relativeUrl and url point to the same resource. The url is the base path + the relative url.
|
51
|
-
Example: browser_path = https://ft.com, relativeUrl = /capital-markets then url = https://www.ft.com/capital-markets.
|
52
|
-
|
53
|
-
Note: we don't need to compute these urls in the business logic of these components as they're passed in as props.
|
54
|
-
|
55
|
-
This note is just an explanation for why relativeUrl has preference over url.*/}
|
56
|
-
<a
|
57
|
-
href={relativeUrl || url}
|
58
|
-
{...generateTrackableProps(conceptTrackable, 'concept')}
|
59
|
-
className='concept-list__concept'>
|
60
|
-
{prefLabel}
|
61
|
-
</a>
|
62
|
-
<FollowButton csrfToken={csrfToken} cacheablePersonalisedUrl={cacheablePersonalisedUrl} conceptId={id} name={prefLabel} flags={flags} />
|
63
|
-
</li>
|
64
|
-
)
|
65
|
-
})}
|
66
|
-
</ul>
|
67
|
-
</div>}
|
68
|
-
</Fragment>)
|
69
|
-
}
|
@@ -1,116 +0,0 @@
|
|
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
|
-
});
|
@@ -1,20 +0,0 @@
|
|
1
|
-
import React from 'react';
|
2
|
-
|
3
|
-
export default function CsrfToken ({ cacheablePersonalisedUrl, csrfToken }) {
|
4
|
-
|
5
|
-
let inputProps = {};
|
6
|
-
|
7
|
-
if (cacheablePersonalisedUrl) {
|
8
|
-
inputProps.value = csrfToken;
|
9
|
-
}
|
10
|
-
|
11
|
-
return (
|
12
|
-
<input
|
13
|
-
data-myft-csrf-token
|
14
|
-
{...inputProps}
|
15
|
-
type="hidden"
|
16
|
-
name="token"
|
17
|
-
/>
|
18
|
-
);
|
19
|
-
|
20
|
-
}
|
@@ -1,23 +0,0 @@
|
|
1
|
-
import React from 'react';
|
2
|
-
import CsrfToken from './input';
|
3
|
-
import { render } from '@testing-library/react';
|
4
|
-
import '@testing-library/jest-dom';
|
5
|
-
|
6
|
-
const props = {
|
7
|
-
cacheablePersonalisedUrl: false
|
8
|
-
};
|
9
|
-
|
10
|
-
describe('Csrf Token Input', () => {
|
11
|
-
|
12
|
-
test('It renders default button', async () => {
|
13
|
-
let { container } = render(<CsrfToken {...props} />);
|
14
|
-
expect(container.querySelector('[name=\'token\']')).toBeTruthy();
|
15
|
-
});
|
16
|
-
|
17
|
-
test('It renders csrf token attribute', async () => {
|
18
|
-
let { container } = render(<CsrfToken cacheablePersonalisedUrl={true} csrfToken={'test-token'} />);
|
19
|
-
expect(container.querySelector('[data-myft-csrf-token]')).toBeTruthy();
|
20
|
-
});
|
21
|
-
|
22
|
-
|
23
|
-
});
|
@@ -1,176 +0,0 @@
|
|
1
|
-
import React, {Fragment} from 'react';
|
2
|
-
import CsrfToken from '../csrf-token/input';
|
3
|
-
|
4
|
-
function generateFormProps (props) {
|
5
|
-
let generatedProps = {};
|
6
|
-
|
7
|
-
const {
|
8
|
-
collectionName,
|
9
|
-
followPlusDigestEmail,
|
10
|
-
conceptId,
|
11
|
-
setFollowButtonStateToSelected,
|
12
|
-
cacheablePersonalisedUrl
|
13
|
-
} = props;
|
14
|
-
|
15
|
-
if (collectionName) {
|
16
|
-
generatedProps['data-myft-tracking'] = `collectionName=${collectionName}`;
|
17
|
-
}
|
18
|
-
|
19
|
-
if(followPlusDigestEmail) {
|
20
|
-
generatedProps['action'] = `/__myft/api/core/follow-plus-digest-email/${conceptId}?method=put`;
|
21
|
-
generatedProps['data-myft-ui-variant'] = 'followPlusDigestEmail';
|
22
|
-
} else {
|
23
|
-
if(setFollowButtonStateToSelected && cacheablePersonalisedUrl) {
|
24
|
-
generatedProps['action'] = `/myft/remove/${conceptId}`;
|
25
|
-
generatedProps['data-js-action'] = `/__myft/api/core/followed/concept/${conceptId}?method=delete`;
|
26
|
-
} else {
|
27
|
-
generatedProps['action'] = `/myft/add/${conceptId}`;
|
28
|
-
generatedProps['data-js-action'] = `/__myft/api/core/followed/concept/${conceptId}?method=put`;
|
29
|
-
}
|
30
|
-
}
|
31
|
-
|
32
|
-
return generatedProps;
|
33
|
-
|
34
|
-
}
|
35
|
-
|
36
|
-
function generateButtonProps (props) {
|
37
|
-
|
38
|
-
const {
|
39
|
-
cacheablePersonalisedUrl,
|
40
|
-
setFollowButtonStateToSelected,
|
41
|
-
name,
|
42
|
-
buttonText,
|
43
|
-
variant,
|
44
|
-
conceptId,
|
45
|
-
alternateText,
|
46
|
-
followPlusDigestEmail
|
47
|
-
} = props;
|
48
|
-
|
49
|
-
let generatedProps = {
|
50
|
-
'data-concept-id': conceptId,
|
51
|
-
'n-myft-follow-button': 'true',
|
52
|
-
'data-trackable': 'follow',
|
53
|
-
type: 'submit'
|
54
|
-
};
|
55
|
-
|
56
|
-
if (cacheablePersonalisedUrl && setFollowButtonStateToSelected) {
|
57
|
-
generatedProps['aria-label'] = `Remove ${name} from myFT`;
|
58
|
-
generatedProps['title'] = `Remove ${name} from myFT`
|
59
|
-
generatedProps['data-alternate-label'] = `Add ${name} to myFT`;
|
60
|
-
generatedProps['aria-pressed'] = true;
|
61
|
-
|
62
|
-
if(alternateText) {
|
63
|
-
generatedProps['data-alternate-text'] = alternateText;
|
64
|
-
} else {
|
65
|
-
if(buttonText) {
|
66
|
-
generatedProps['data-alternate-text'] = buttonText;
|
67
|
-
} else {
|
68
|
-
generatedProps['data-alternate-text'] = 'Add to myFT';
|
69
|
-
}
|
70
|
-
}
|
71
|
-
} else {
|
72
|
-
generatedProps['aria-pressed'] = false;
|
73
|
-
generatedProps['aria-label'] = `Add ${name} to myFT`;
|
74
|
-
generatedProps['title'] = `Add ${name} to myFT`;
|
75
|
-
generatedProps['data-alternate-label'] = `Remove ${name} from myFT`;
|
76
|
-
if (alternateText) {
|
77
|
-
generatedProps['data-alternate-text'] = alternateText;
|
78
|
-
} else {
|
79
|
-
if (buttonText) {
|
80
|
-
generatedProps['data-alternate-text'] = buttonText;
|
81
|
-
} else {
|
82
|
-
generatedProps['data-alternate-text'] = 'Added';
|
83
|
-
}
|
84
|
-
}
|
85
|
-
}
|
86
|
-
|
87
|
-
if(variant) {
|
88
|
-
generatedProps[`n-myft-follow-button--${variant}`] = 'true';
|
89
|
-
}
|
90
|
-
|
91
|
-
if(followPlusDigestEmail) {
|
92
|
-
generatedProps['data-trackable-context-messaging'] = 'add-to-myft-plus-digest-button';
|
93
|
-
}
|
94
|
-
|
95
|
-
return generatedProps;
|
96
|
-
}
|
97
|
-
|
98
|
-
function getButtonText (props) {
|
99
|
-
|
100
|
-
const {
|
101
|
-
buttonText,
|
102
|
-
setFollowButtonStateToSelected,
|
103
|
-
cacheablePersonalisedUrl
|
104
|
-
} = props;
|
105
|
-
let outputText;
|
106
|
-
|
107
|
-
if(buttonText) {
|
108
|
-
outputText = buttonText;
|
109
|
-
} else {
|
110
|
-
if(setFollowButtonStateToSelected && cacheablePersonalisedUrl) {
|
111
|
-
outputText = 'Added';
|
112
|
-
} else {
|
113
|
-
outputText = 'Add to myFT';
|
114
|
-
}
|
115
|
-
}
|
116
|
-
|
117
|
-
return outputText;
|
118
|
-
}
|
119
|
-
|
120
|
-
/**
|
121
|
-
*
|
122
|
-
* @param {Object} props
|
123
|
-
* @param {string} props.name
|
124
|
-
* @param {Object} props.flags
|
125
|
-
* @param {string} props.extraClasses
|
126
|
-
* @param {string} props.conceptId
|
127
|
-
* @param {string} props.variant
|
128
|
-
* @param {string} props.buttonText
|
129
|
-
* @param {*} props.setFollowButtonStateToSelected
|
130
|
-
* @param {string} props.cacheablePersonalisedUrl
|
131
|
-
* @param {string} props.alternateText
|
132
|
-
* @param {*} props.followPlusDigestEmail
|
133
|
-
* @param {string} props.collectionName
|
134
|
-
*/
|
135
|
-
export default function FollowButton (props) {
|
136
|
-
|
137
|
-
const {
|
138
|
-
name,
|
139
|
-
flags,
|
140
|
-
extraClasses,
|
141
|
-
conceptId,
|
142
|
-
variant,
|
143
|
-
csrfToken,
|
144
|
-
cacheablePersonalisedUrl
|
145
|
-
} = props;
|
146
|
-
|
147
|
-
const formProps = generateFormProps(props);
|
148
|
-
const buttonProps = generateButtonProps(props);
|
149
|
-
|
150
|
-
const getVariantClass = (variant) => variant ? `n-myft-follow-button--${variant}` : '';
|
151
|
-
|
152
|
-
return (
|
153
|
-
<Fragment>
|
154
|
-
{flags.myFtApiWrite && <form
|
155
|
-
className={`n-myft-ui n-myft-ui--follow ${extraClasses || ''}`}
|
156
|
-
method="GET"
|
157
|
-
data-myft-ui="follow"
|
158
|
-
data-concept-id={conceptId}
|
159
|
-
{...formProps}>
|
160
|
-
<CsrfToken cacheablePersonalisedUrl={cacheablePersonalisedUrl} csrfToken={csrfToken} />
|
161
|
-
<div
|
162
|
-
className="n-myft-ui__announcement o-normalise-visually-hidden"
|
163
|
-
aria-live="assertive"
|
164
|
-
data-pressed-text={`Now following ${name}.`}
|
165
|
-
data-unpressed-text={`No longer following ${name}.`}
|
166
|
-
></div>
|
167
|
-
<button
|
168
|
-
{...buttonProps}
|
169
|
-
className={[`n-myft-follow-button ${getVariantClass(variant)}`]}>
|
170
|
-
{getButtonText(props)}
|
171
|
-
</button>
|
172
|
-
</form>}
|
173
|
-
</Fragment>
|
174
|
-
);
|
175
|
-
|
176
|
-
}
|
@@ -1,40 +0,0 @@
|
|
1
|
-
import React from 'react';
|
2
|
-
import FollowButton from './follow-button';
|
3
|
-
import { render, screen } from '@testing-library/react';
|
4
|
-
import '@testing-library/jest-dom';
|
5
|
-
|
6
|
-
const props = {
|
7
|
-
flags: {
|
8
|
-
myFtApi: true,
|
9
|
-
myFtApiWrite: true
|
10
|
-
},
|
11
|
-
conceptId: '0000-000000-00000-0000',
|
12
|
-
name: 'Follow button'
|
13
|
-
};
|
14
|
-
|
15
|
-
describe('Follow button', () => {
|
16
|
-
|
17
|
-
test('It renders default button', async () => {
|
18
|
-
render(<FollowButton {...props} />);
|
19
|
-
expect(await screen.findByText('Add to myFT')).toBeTruthy();
|
20
|
-
});
|
21
|
-
|
22
|
-
test('It renders a variant', async () => {
|
23
|
-
const { container } = render(<FollowButton {...props} variant={'standard'} />);
|
24
|
-
expect(container.getElementsByClassName('n-myft-follow-button--standard')).toHaveLength(1);
|
25
|
-
});
|
26
|
-
|
27
|
-
test('It renders follow button form', async () => {
|
28
|
-
const { container } = render(<FollowButton {...props} variant={'standard'} />);
|
29
|
-
expect(container.querySelector(`form[action='/myft/add/${props.conceptId}']`)).toBeTruthy();
|
30
|
-
});
|
31
|
-
|
32
|
-
test('Button state changes when attributes change', async () => {
|
33
|
-
render(<FollowButton {...props}
|
34
|
-
variant={'standard'}
|
35
|
-
setFollowButtonStateToSelected={true}
|
36
|
-
cacheablePersonalisedUrl={true} />);
|
37
|
-
expect(await screen.findByText('Added')).toBeTruthy();
|
38
|
-
});
|
39
|
-
|
40
|
-
});
|
package/components/index.js
DELETED
@@ -1,17 +0,0 @@
|
|
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
|
-
import InstantAlert from './instant-alert/instant-alert';
|
8
|
-
|
9
|
-
export {
|
10
|
-
CsrfToken,
|
11
|
-
FollowButton,
|
12
|
-
ConceptList,
|
13
|
-
Collections,
|
14
|
-
SaveForLater,
|
15
|
-
PinButton,
|
16
|
-
InstantAlert
|
17
|
-
};
|
@@ -1,73 +0,0 @@
|
|
1
|
-
import React, { Fragment } from 'react';
|
2
|
-
import CsrfToken from '../csrf-token/input';
|
3
|
-
|
4
|
-
/**
|
5
|
-
*
|
6
|
-
* @param {Object} props
|
7
|
-
* @param {string} props.name
|
8
|
-
* @param {Object} props.flags
|
9
|
-
* @param {booelan} props.hideButtonText
|
10
|
-
* @param {string} props.conceptId
|
11
|
-
* @param {string} props.name
|
12
|
-
* @param {string} props.extraClasses
|
13
|
-
* @param {boolean} props.directType
|
14
|
-
* @param {string} props.cacheablePersonalisedUrl
|
15
|
-
* @param {string} props.hasInstantAlert
|
16
|
-
* @param {string} props.buttonText
|
17
|
-
* @param {string} props.alternateText
|
18
|
-
* @param {string} props.variant
|
19
|
-
* @param {string} props.size
|
20
|
-
*/
|
21
|
-
export default function InstantAlert (props) {
|
22
|
-
|
23
|
-
const {
|
24
|
-
hasInstantAlert,
|
25
|
-
cacheablePersonalisedUrl,
|
26
|
-
name,
|
27
|
-
alternateText,
|
28
|
-
buttonText,
|
29
|
-
conceptId,
|
30
|
-
variant,
|
31
|
-
size,
|
32
|
-
flags,
|
33
|
-
hideButtonText,
|
34
|
-
directType,
|
35
|
-
extraClasses
|
36
|
-
} = props;
|
37
|
-
|
38
|
-
const generateButtonProps = () => {
|
39
|
-
|
40
|
-
let buttonProps = {
|
41
|
-
'aria-pressed': `${Boolean(hasInstantAlert) && Boolean(cacheablePersonalisedUrl)}`,
|
42
|
-
'aria-label': `Get instant alerts for ${name}`,
|
43
|
-
'data-alternate-label': `Stop instant alerts for ${name}`,
|
44
|
-
'data-alternate-text': alternateText? alternateText: (buttonText ? buttonText : 'Instant alerts'),
|
45
|
-
'data-concept-id': conceptId, // duplicated here for tracking
|
46
|
-
'data-trackable': 'instant',
|
47
|
-
title: `Get instant alerts for ${name}`,
|
48
|
-
value: hasInstantAlert ? false : true,
|
49
|
-
type: 'submit',
|
50
|
-
name: '_rel.instant',
|
51
|
-
className: `n-myft-ui__button n-myft-ui__button--instant n-myft-ui__button--instant-light${variant ? ` n-myft-ui__button--${variant}` : ''}${size ? ` n-myft-ui__button--${size}` : ''}`
|
52
|
-
};
|
53
|
-
return buttonProps;
|
54
|
-
}
|
55
|
-
|
56
|
-
return (
|
57
|
-
<Fragment>
|
58
|
-
{flags.myFtApiWrite &&
|
59
|
-
<form className={`n-myft-ui n-myft-ui--instant${hideButtonText ? ' n-myft-ui--instant--hide-text' : ''}${extraClasses ? ` ${extraClasses}` : ''}`}
|
60
|
-
method="GET"
|
61
|
-
data-myft-ui="instant"
|
62
|
-
data-concept-id={conceptId}
|
63
|
-
action={`/myft/add/${conceptId}?instant=true`}
|
64
|
-
data-js-action={`/__myft/api/core/followed/concept/${conceptId}?method=put`}>
|
65
|
-
<CsrfToken />
|
66
|
-
<input type="hidden" value={name} name="name" />
|
67
|
-
<input type="hidden" value={directType || 'http://www.ft.com/ontology/concept/Concept'} name="directType" />
|
68
|
-
<button {...generateButtonProps()}>{buttonText ? buttonText : 'Instant alerts'}</button>
|
69
|
-
</form>}
|
70
|
-
</Fragment>
|
71
|
-
);
|
72
|
-
|
73
|
-
}
|
@@ -1,86 +0,0 @@
|
|
1
|
-
import React from 'react';
|
2
|
-
import InstantAlert from './instant-alert';
|
3
|
-
import { render, screen } from '@testing-library/react';
|
4
|
-
import '@testing-library/jest-dom';
|
5
|
-
|
6
|
-
const props = {
|
7
|
-
flags: {
|
8
|
-
myFtApi: true,
|
9
|
-
myFtApiWrite: true
|
10
|
-
},
|
11
|
-
conceptId: '0000-000000-00000-0000',
|
12
|
-
name: 'Instant Alert',
|
13
|
-
buttonText: 'Instant Alert'
|
14
|
-
};
|
15
|
-
|
16
|
-
describe('InstantAlert', () => {
|
17
|
-
|
18
|
-
test('It renders', async () => {
|
19
|
-
render(<InstantAlert {...props} />);
|
20
|
-
expect(await screen.findByText('Instant Alert')).toBeInTheDocument();
|
21
|
-
});
|
22
|
-
|
23
|
-
test('It renders form attributes', () => {
|
24
|
-
const { container } = render(<InstantAlert {...props} />);
|
25
|
-
expect(container.querySelector('form[method="GET"]')).toBeInTheDocument();
|
26
|
-
expect(container.querySelector(`form[action='/myft/add/${props.conceptId}?instant=true']`)).toBeInTheDocument();
|
27
|
-
expect(container.querySelector('form[data-myft-ui="instant"]')).toBeInTheDocument();
|
28
|
-
expect(container.querySelector(`form[data-concept-id="${props.conceptId}"]`)).toBeInTheDocument();
|
29
|
-
expect(container.querySelector(`form[data-js-action="/__myft/api/core/followed/concept/${props.conceptId}?method=put"]`)).toBeInTheDocument();
|
30
|
-
});
|
31
|
-
|
32
|
-
test('It renders extraClasses in form', () => {
|
33
|
-
const { container } = render(<InstantAlert {...props} extraClasses={'extra'} />);
|
34
|
-
expect(container.querySelector('form[class="n-myft-ui n-myft-ui--instant extra"]')).toBeInTheDocument();
|
35
|
-
});
|
36
|
-
|
37
|
-
test('It renders hide button class in form', () => {
|
38
|
-
const { container } = render(<InstantAlert {...props} hideButtonText={true} />);
|
39
|
-
expect(container.querySelector('form[class="n-myft-ui n-myft-ui--instant n-myft-ui--instant--hide-text"]')).toBeInTheDocument();
|
40
|
-
});
|
41
|
-
|
42
|
-
test('It renders csrftoken input', () => {
|
43
|
-
const { container } = render(<InstantAlert {...props} />);
|
44
|
-
expect(container.querySelector('input[data-myft-csrf-token]')).toBeInTheDocument();
|
45
|
-
});
|
46
|
-
|
47
|
-
test('It renders input name as value attribute', () => {
|
48
|
-
const { container } = render(<InstantAlert {...props} />);
|
49
|
-
expect(container.querySelector('input[value="Instant Alert"]')).toBeInTheDocument();
|
50
|
-
});
|
51
|
-
|
52
|
-
test('It renders input directType as value attribute', () => {
|
53
|
-
const { container } = render(<InstantAlert {...props} directType={'http://www.ft.com/ontology/test/Test'} />);
|
54
|
-
expect(container.querySelector('input[value="http://www.ft.com/ontology/test/Test"]')).toBeInTheDocument();
|
55
|
-
});
|
56
|
-
|
57
|
-
test('It renders button props', () => {
|
58
|
-
const { container } = render(<InstantAlert {...props} alternateText={'Sample alternate text'} variant={'blue'} size={'small'} />);
|
59
|
-
expect(container.querySelector(`button[aria-label="Get instant alerts for ${props.name}"]`)).toBeInTheDocument();
|
60
|
-
expect(container.querySelector('button[data-alternate-text="Sample alternate text"]')).toBeInTheDocument();
|
61
|
-
expect(container.querySelector(`button[data-concept-id="${props.conceptId}"]`)).toBeInTheDocument();
|
62
|
-
expect(container.querySelector('button[data-trackable="instant"]')).toBeInTheDocument();
|
63
|
-
expect(container.querySelector(`button[title="Get instant alerts for ${props.name}"]`)).toBeInTheDocument();
|
64
|
-
expect(container.querySelector('button[value="true"]')).toBeInTheDocument();
|
65
|
-
expect(container.querySelector('button[type="submit"]')).toBeInTheDocument();
|
66
|
-
expect(container.querySelector('button[name="_rel.instant"]')).toBeInTheDocument();
|
67
|
-
expect(container.getElementsByClassName('n-myft-ui__button--blue')).toHaveLength(1);
|
68
|
-
expect(container.getElementsByClassName('n-myft-ui__button--small')).toHaveLength(1);
|
69
|
-
});
|
70
|
-
|
71
|
-
test('It renders buttonText as data-alternate-text attribute when alternateText prop is not provided', () => {
|
72
|
-
const { container } = render(<InstantAlert {...props} buttonText={'Sample button text'} variant={'blue'} size={'small'} />);
|
73
|
-
expect(container.querySelector('button[data-alternate-text="Sample button text"]')).toBeInTheDocument();
|
74
|
-
});
|
75
|
-
|
76
|
-
test('It renders button aria-pressed=false attribute when hasInstantAlert=false or cacheablePersonalisedUrl props is not provided', () => {
|
77
|
-
render(<InstantAlert {...props} buttonText={'Sample button text'} hasInstantAlert={false} cacheablePersonalisedUrl={'https://ft.com'} />);
|
78
|
-
expect(screen.getByRole('button', {pressed: false})).toBeInTheDocument();
|
79
|
-
});
|
80
|
-
|
81
|
-
test('It renders button aria-pressed=true attribute when hasInstantAlert=true and cacheablePersonalisedUrl provided', () => {
|
82
|
-
render(<InstantAlert {...props} buttonText={'Sample button text'} hasInstantAlert={true} cacheablePersonalisedUrl={'https://ft.com'} />);
|
83
|
-
expect(screen.getByRole('button', {pressed: true})).toBeInTheDocument();
|
84
|
-
});
|
85
|
-
|
86
|
-
});
|
@@ -1,40 +0,0 @@
|
|
1
|
-
import React, { Fragment } from 'react';
|
2
|
-
import CsrfToken from '../csrf-token/input';
|
3
|
-
export default function PinButton ({ showPrioritiseButton, id, name, directType, prioritised, csrfToken, cacheablePersonalisedUrl }) {
|
4
|
-
|
5
|
-
const getAction = () => `/__myft/api/core/prioritised/concept/${id}?method=${prioritised ? 'delete' : 'put'}`;
|
6
|
-
|
7
|
-
if (!showPrioritiseButton) {
|
8
|
-
return null;
|
9
|
-
}
|
10
|
-
|
11
|
-
return (
|
12
|
-
<Fragment>
|
13
|
-
<span className="myft-pin-divider"></span>
|
14
|
-
<div className="myft-pin-button-wrapper">
|
15
|
-
<form method="post" action={getAction()} data-myft-prioritise>
|
16
|
-
<CsrfToken csrfToken={csrfToken} cacheablePersonalisedUrl={cacheablePersonalisedUrl} />
|
17
|
-
<input type="hidden" value={name} name="name" />
|
18
|
-
<input type="hidden" value={directType || 'http://www.ft.com/ontology/concept/Concept'} name="directType" />
|
19
|
-
<div
|
20
|
-
className="n-myft-ui__announcement o-normalise-visually-hidden"
|
21
|
-
aria-live="assertive"
|
22
|
-
data-pressed-text={`${name} pinned in myFT.`}
|
23
|
-
data-unpressed-text={`Unpinned ${name} from myFT.`}
|
24
|
-
></div>
|
25
|
-
<button id={`myft-pin-button__${id}`}
|
26
|
-
className="myft-pin-button"
|
27
|
-
data-prioritise-button
|
28
|
-
data-trackable="prioritised"
|
29
|
-
data-concept-id={id}
|
30
|
-
data-prioritised={prioritised ? true : false}
|
31
|
-
aria-label={`${prioritised ? 'Unpin' : 'Pin'} ${name} ${prioritised ? 'from' : 'in'} my F T`}
|
32
|
-
aria-pressed={prioritised ? true : false}
|
33
|
-
title={`${prioritised ? 'Unpin' : 'Pin'} ${name}`}>
|
34
|
-
</button>
|
35
|
-
</form>
|
36
|
-
</div>
|
37
|
-
</Fragment>
|
38
|
-
)
|
39
|
-
|
40
|
-
}
|