@financial-times/n-myft-ui 25.0.0 → 26.1.0
Sign up to get free protection for your applications and to get access to all the features.
- package/.circleci/config.yml +26 -12
- package/README.md +2 -3
- package/build-state/npm-shrinkwrap.json +18704 -19936
- package/components/collections/collections.jsx +3 -3
- package/components/concept-list/concept-list.jsx +19 -5
- 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 +3 -1
- package/components/follow-button/{__tests__/follow-button.test.js → follow-button.test.js} +1 -1
- package/components/index.js +3 -1
- 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 +30 -30
- package/components/pin-button/pin-button.test.js +2 -2
- package/components/save-for-later/save-for-later.jsx +19 -21
- package/demos/templates/demo.html +1 -2
- package/dist/bundles/bundle.js +174 -75
- package/jsx-migration.md +16 -0
- package/package.json +2 -2
- package/components/instant-alert/instant-alert.html +0 -47
@@ -2,7 +2,7 @@ import React from 'react';
|
|
2
2
|
import CsrfToken from '../csrf-token/input';
|
3
3
|
import FollowButton from '../follow-button/follow-button';
|
4
4
|
|
5
|
-
export default function Collections ({ title, liteStyle, flags, collectionName, trackable, concepts = [] }) {
|
5
|
+
export default function Collections ({ title, liteStyle, flags, collectionName, trackable, concepts = [], csrfToken, cacheablePersonalisedUrl }) {
|
6
6
|
const getLiteStyleModifier = () => liteStyle ? 'lite' : 'regular';
|
7
7
|
let formProps = {
|
8
8
|
method: 'POST',
|
@@ -27,7 +27,7 @@ export default function Collections ({ title, liteStyle, flags, collectionName,
|
|
27
27
|
<ul className="collection__concepts">
|
28
28
|
{concepts && concepts.map((concept, index) =>
|
29
29
|
<li className="collection__concept" key={index}>
|
30
|
-
<FollowButton variant={liteStyle ? 'primary' : 'inverse'} buttonText={concept.name} flags={flags} collectionName={collectionName} />
|
30
|
+
<FollowButton cacheablePersonalisedUrl={cacheablePersonalisedUrl} csrfToken={csrfToken} variant={liteStyle ? 'primary' : 'inverse'} buttonText={concept.name} flags={flags} collectionName={collectionName} />
|
31
31
|
</li>)
|
32
32
|
}
|
33
33
|
</ul>
|
@@ -41,7 +41,7 @@ export default function Collections ({ title, liteStyle, flags, collectionName,
|
|
41
41
|
name="directType"
|
42
42
|
value={concepts.map(concept => concept.directType).join(',')}
|
43
43
|
/>
|
44
|
-
<CsrfToken />
|
44
|
+
<CsrfToken csrfToken={csrfToken} cacheablePersonalisedUrl={cacheablePersonalisedUrl} />
|
45
45
|
<input
|
46
46
|
type="hidden"
|
47
47
|
name="name"
|
@@ -1,24 +1,32 @@
|
|
1
1
|
import React, { Fragment } from 'react';
|
2
2
|
import FollowButton from '../follow-button/follow-button';
|
3
3
|
|
4
|
-
export default function ConceptList ({ flags, concepts, contentType, conceptListTitle, trackable }) {
|
4
|
+
export default function ConceptList ({ flags, concepts, contentType, conceptListTitle, trackable, csrfToken, cacheablePersonalisedUrl }) {
|
5
5
|
|
6
6
|
const {
|
7
7
|
myFtApi,
|
8
8
|
myFtApiWrite
|
9
9
|
} = flags;
|
10
10
|
|
11
|
-
const generateTrackableProps = (primary,
|
11
|
+
const generateTrackableProps = (primary, secondary) => {
|
12
12
|
return {
|
13
|
-
'data-trackable': primary ? primary :
|
13
|
+
'data-trackable': primary ? primary : secondary
|
14
14
|
}
|
15
15
|
}
|
16
16
|
|
17
|
+
const shouldDisplay = () => {
|
18
|
+
if(myFtApi && myFtApiWrite && Array.isArray(concepts) && concepts.length) {
|
19
|
+
return true
|
20
|
+
}
|
21
|
+
|
22
|
+
return false;
|
23
|
+
}
|
24
|
+
|
17
25
|
|
18
26
|
return (
|
19
27
|
|
20
28
|
<Fragment>
|
21
|
-
{(
|
29
|
+
{shouldDisplay() &&
|
22
30
|
<div
|
23
31
|
className='concept-list'
|
24
32
|
{...generateTrackableProps(trackable, 'concept-list')}>
|
@@ -39,13 +47,19 @@ export default function ConceptList ({ flags, concepts, contentType, conceptList
|
|
39
47
|
} = concept;
|
40
48
|
return (
|
41
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.*/}
|
42
56
|
<a
|
43
57
|
href={relativeUrl || url}
|
44
58
|
{...generateTrackableProps(conceptTrackable, 'concept')}
|
45
59
|
className='concept-list__concept'>
|
46
60
|
{prefLabel}
|
47
61
|
</a>
|
48
|
-
<FollowButton conceptId={id} name={prefLabel} flags={flags} />
|
62
|
+
<FollowButton csrfToken={csrfToken} cacheablePersonalisedUrl={cacheablePersonalisedUrl} conceptId={id} name={prefLabel} flags={flags} />
|
49
63
|
</li>
|
50
64
|
)
|
51
65
|
})}
|
@@ -5,18 +5,12 @@ export default function CsrfToken ({ cacheablePersonalisedUrl, csrfToken }) {
|
|
5
5
|
let inputProps = {};
|
6
6
|
|
7
7
|
if (cacheablePersonalisedUrl) {
|
8
|
-
inputProps = {
|
9
|
-
...inputProps,
|
10
|
-
'data-myft-csrf-token': csrfToken
|
11
|
-
};
|
12
|
-
}
|
13
|
-
|
14
|
-
if(csrfToken) {
|
15
8
|
inputProps.value = csrfToken;
|
16
9
|
}
|
17
10
|
|
18
11
|
return (
|
19
12
|
<input
|
13
|
+
data-myft-csrf-token
|
20
14
|
{...inputProps}
|
21
15
|
type="hidden"
|
22
16
|
name="token"
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import React from 'react';
|
2
|
-
import CsrfToken from '
|
2
|
+
import CsrfToken from './input';
|
3
3
|
import { render } from '@testing-library/react';
|
4
4
|
import '@testing-library/jest-dom';
|
5
5
|
|
@@ -16,7 +16,7 @@ describe('Csrf Token Input', () => {
|
|
16
16
|
|
17
17
|
test('It renders csrf token attribute', async () => {
|
18
18
|
let { container } = render(<CsrfToken cacheablePersonalisedUrl={true} csrfToken={'test-token'} />);
|
19
|
-
expect(container.querySelector('[data-myft-csrf-token
|
19
|
+
expect(container.querySelector('[data-myft-csrf-token]')).toBeTruthy();
|
20
20
|
});
|
21
21
|
|
22
22
|
|
@@ -140,6 +140,8 @@ export default function FollowButton (props) {
|
|
140
140
|
extraClasses,
|
141
141
|
conceptId,
|
142
142
|
variant,
|
143
|
+
csrfToken,
|
144
|
+
cacheablePersonalisedUrl
|
143
145
|
} = props;
|
144
146
|
|
145
147
|
const formProps = generateFormProps(props);
|
@@ -155,7 +157,7 @@ export default function FollowButton (props) {
|
|
155
157
|
data-myft-ui="follow"
|
156
158
|
data-concept-id={conceptId}
|
157
159
|
{...formProps}>
|
158
|
-
<CsrfToken cacheablePersonalisedUrl={
|
160
|
+
<CsrfToken cacheablePersonalisedUrl={cacheablePersonalisedUrl} csrfToken={csrfToken} />
|
159
161
|
<div
|
160
162
|
className="n-myft-ui__announcement o-normalise-visually-hidden"
|
161
163
|
aria-live="assertive"
|
package/components/index.js
CHANGED
@@ -4,6 +4,7 @@ import ConceptList from './concept-list/concept-list';
|
|
4
4
|
import Collections from './collections/collections';
|
5
5
|
import SaveForLater from './save-for-later/save-for-later';
|
6
6
|
import PinButton from './pin-button/pin-button';
|
7
|
+
import InstantAlert from './instant-alert/instant-alert';
|
7
8
|
|
8
9
|
export {
|
9
10
|
CsrfToken,
|
@@ -11,5 +12,6 @@ export {
|
|
11
12
|
ConceptList,
|
12
13
|
Collections,
|
13
14
|
SaveForLater,
|
14
|
-
PinButton
|
15
|
+
PinButton,
|
16
|
+
InstantAlert
|
15
17
|
};
|
@@ -0,0 +1,73 @@
|
|
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
|
+
}
|
@@ -0,0 +1,86 @@
|
|
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,39 +1,39 @@
|
|
1
1
|
import React, { Fragment } from 'react';
|
2
2
|
import CsrfToken from '../csrf-token/input';
|
3
|
-
export default function PinButton ({ showPrioritiseButton, id, name, directType, prioritised }) {
|
3
|
+
export default function PinButton ({ showPrioritiseButton, id, name, directType, prioritised, csrfToken, cacheablePersonalisedUrl }) {
|
4
4
|
|
5
|
-
const getAction = () => `/__myft/api/core/prioritised/concept/${id}?method=${prioritised ? 'delete' : 'put'}
|
5
|
+
const getAction = () => `/__myft/api/core/prioritised/concept/${id}?method=${prioritised ? 'delete' : 'put'}`;
|
6
|
+
|
7
|
+
if (!showPrioritiseButton) {
|
8
|
+
return null;
|
9
|
+
}
|
6
10
|
|
7
11
|
return (
|
8
12
|
<Fragment>
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
<
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
</form>
|
34
|
-
</div>
|
35
|
-
</Fragment>
|
36
|
-
}
|
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
37
|
</Fragment>
|
38
38
|
)
|
39
39
|
|
@@ -34,7 +34,7 @@ describe('Pin Button', () => {
|
|
34
34
|
|
35
35
|
test('It renders unprioritised', () => {
|
36
36
|
const { container } = render(<PinButton flags={flags} {...fixtures[0]} />);
|
37
|
-
expect(container.querySelector('button[aria-label="Pin myFT Enterprises in
|
37
|
+
expect(container.querySelector('button[aria-label="Pin myFT Enterprises in my F T"]')).toBeTruthy();
|
38
38
|
expect(container.querySelector('button[title="Pin myFT Enterprises"]')).toBeTruthy();
|
39
39
|
expect(container.querySelector('button[data-prioritised=false]')).toBeTruthy();
|
40
40
|
expect(container.querySelector(`button[data-concept-id="${fixtures[0].id}"]`)).toBeTruthy();
|
@@ -42,7 +42,7 @@ describe('Pin Button', () => {
|
|
42
42
|
|
43
43
|
test('It renders with prioritised', () => {
|
44
44
|
const { container } = render(<PinButton flags={flags} prioritised={true} {...fixtures[0]} />);
|
45
|
-
expect(container.querySelector('button[aria-label="Unpin myFT Enterprises from
|
45
|
+
expect(container.querySelector('button[aria-label="Unpin myFT Enterprises from my F T"]')).toBeTruthy();
|
46
46
|
expect(container.querySelector('button[title="Unpin myFT Enterprises"]')).toBeTruthy();
|
47
47
|
expect(container.querySelector('button[data-prioritised=true]')).toBeTruthy();
|
48
48
|
expect(container.querySelector(`button[data-concept-id="${fixtures[0].id}"]`)).toBeTruthy();
|
@@ -3,6 +3,19 @@ import CsrfToken from '../csrf-token/input';
|
|
3
3
|
|
4
4
|
const ButtonContent = ({ saveButtonWithIcon, buttonText, isSaved, appIsStreamPage }) => {
|
5
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
|
+
|
6
19
|
return (<Fragment>
|
7
20
|
{
|
8
21
|
saveButtonWithIcon &&
|
@@ -16,28 +29,13 @@ const ButtonContent = ({ saveButtonWithIcon, buttonText, isSaved, appIsStreamPag
|
|
16
29
|
!saveButtonWithIcon &&
|
17
30
|
<Fragment>
|
18
31
|
{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>
|
32
|
+
{!buttonText && <DefaultButtonText />
|
35
33
|
}
|
36
34
|
</Fragment>
|
37
35
|
}
|
38
36
|
</Fragment>);
|
39
37
|
}
|
40
|
-
export default function SaveForLater
|
38
|
+
export default function SaveForLater({ flags, contentId, title, variant, trackableId, isSaved, appIsStreamPage, alternateText, saveButtonWithIcon, buttonText, csrfToken, cacheablePersonalisedUrl }) {
|
41
39
|
|
42
40
|
const { myFtApiWrite } = flags;
|
43
41
|
|
@@ -51,7 +49,7 @@ export default function SaveForLater ({ flags, contentId, title, variant, tracka
|
|
51
49
|
};
|
52
50
|
|
53
51
|
if (isSaved) {
|
54
|
-
let titleText = `${title ? `${title} is` : ''}
|
52
|
+
let titleText = `${title ? `${title} is` : ''} saved to myFT`;
|
55
53
|
props['title'] = title;
|
56
54
|
props['aria-label'] = titleText;
|
57
55
|
props['data-alternate-label'] = title ? `Save ${title} to myFT for later` : 'Save this article to myFT for later';
|
@@ -60,7 +58,7 @@ export default function SaveForLater ({ flags, contentId, title, variant, tracka
|
|
60
58
|
let titleText = title ? `Save ${title} to myFT for later` : 'Save this article to myFT for later';
|
61
59
|
props['title'] = titleText;
|
62
60
|
props['aria-label'] = titleText;
|
63
|
-
props['data-alternate-label'] = `${title ? `${title} is` : ''}
|
61
|
+
props['data-alternate-label'] = `${title ? `${title} is` : ''} saved to myFT`;
|
64
62
|
props['aria-pressed'] = false;
|
65
63
|
}
|
66
64
|
|
@@ -84,7 +82,7 @@ export default function SaveForLater ({ flags, contentId, title, variant, tracka
|
|
84
82
|
data-myft-ui="saved"
|
85
83
|
action={`/myft/save/${contentId}`}
|
86
84
|
data-js-action={`/__myft/api/core/saved/content/${contentId}?method=put`}>
|
87
|
-
<CsrfToken />
|
85
|
+
<CsrfToken csrfToken={csrfToken} cacheablePersonalisedUrl={cacheablePersonalisedUrl} />
|
88
86
|
|
89
87
|
<div
|
90
88
|
className="n-myft-ui__announcement o-normalise-visually-hidden"
|
@@ -93,7 +91,7 @@ export default function SaveForLater ({ flags, contentId, title, variant, tracka
|
|
93
91
|
data-unpressed-text="Removed article from My FT."
|
94
92
|
></div>
|
95
93
|
<button {...generateSubmitButtonProps()}>
|
96
|
-
<ButtonContent buttonText={buttonText} saveButtonWithIcon={saveButtonWithIcon} isSaved={isSaved} appIsStreamPage={appIsStreamPage}/>
|
94
|
+
<ButtonContent buttonText={buttonText} saveButtonWithIcon={saveButtonWithIcon} isSaved={isSaved} appIsStreamPage={appIsStreamPage} />
|
97
95
|
</button>
|
98
96
|
</form>
|
99
97
|
}
|
@@ -83,10 +83,9 @@
|
|
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>
|