@financial-times/n-myft-ui 23.1.3 → 25.0.0
Sign up to get free protection for your applications and to get access to all the features.
- package/.circleci/config.yml +27 -30
- package/.circleci/shared-helpers/helper-npm-install-peer-deps +6 -5
- package/.github/settings.yml +1 -1
- package/.scss-lint.yml +3 -3
- package/Makefile +1 -0
- package/README.md +62 -8
- package/build-state/npm-shrinkwrap.json +49383 -17508
- package/components/collections/collections.jsx +68 -0
- package/components/collections/collections.test.js +83 -0
- package/components/concept-list/concept-list.jsx +55 -0
- package/components/concept-list/concept-list.test.js +116 -0
- package/components/csrf-token/__tests__/input.test.js +23 -0
- package/components/csrf-token/input.jsx +26 -0
- package/components/follow-button/__tests__/follow-button.test.js +40 -0
- package/components/follow-button/follow-button.jsx +174 -0
- package/components/index.js +15 -0
- package/components/instant-alert/instant-alert.html +1 -1
- 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/components/unread-articles-indicator/date-fns.js +5 -12
- package/demos/app.js +39 -21
- package/demos/templates/demo-layout.html +1 -1
- package/demos/templates/demo.html +436 -415
- package/demos/templates/demo.jsx +125 -0
- package/dist/bundles/bundle.js +3133 -0
- package/jest.config.js +8 -0
- package/package.json +38 -13
- package/webpack.config.js +34 -0
- package/components/collections/collections.html +0 -85
- package/components/concept-list/concept-list.html +0 -31
- package/components/csrf-token/input.html +0 -5
- package/components/follow-button/follow-button.html +0 -79
- package/components/pin-button/pin-button.html +0 -20
- package/components/save-for-later/save-for-later.html +0 -67
- package/demos/fixtures/follow-button-plus-digest.json +0 -6
- package/demos/templates/digest-on-follow.html +0 -12
@@ -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
|
+
});
|
@@ -1,5 +1,5 @@
|
|
1
1
|
// date-fns from v2 doesn't accept String arguments anymore.
|
2
|
-
// the detail => https://github.com/date-fns/date-fns/blob/
|
2
|
+
// the detail => https://github.com/date-fns/date-fns/blob/HEAD/CHANGELOG.md#200---2019-08-20
|
3
3
|
// By adding validation for dates before their functions allows us to know it when unexpected value passed.
|
4
4
|
|
5
5
|
import isTodayOriginal from 'date-fns/src/isToday';
|
@@ -11,22 +11,15 @@ import parseISO from 'date-fns/src/parseISO';
|
|
11
11
|
|
12
12
|
const isValid = (date) => {
|
13
13
|
if (!isValidOriginal(date)) {
|
14
|
-
console.error(
|
14
|
+
console.error("Invalid date passed", [date]); //eslint-disable-line
|
15
15
|
}
|
16
16
|
return date;
|
17
17
|
};
|
18
18
|
|
19
19
|
const isToday = (date) => isTodayOriginal(isValid(date));
|
20
|
-
const isAfter = (date, dateToCompare) =>
|
20
|
+
const isAfter = (date, dateToCompare) =>
|
21
|
+
isAfterOriginal(isValid(date), isValid(dateToCompare));
|
21
22
|
const addMinutes = (date, amount) => addMinutesOriginal(isValid(date), amount);
|
22
23
|
const startOfDay = (date) => startOfDayOriginal(isValid(date));
|
23
24
|
|
24
|
-
|
25
|
-
export {
|
26
|
-
isToday,
|
27
|
-
isAfter,
|
28
|
-
addMinutes,
|
29
|
-
startOfDay,
|
30
|
-
isValid,
|
31
|
-
parseISO,
|
32
|
-
};
|
25
|
+
export { isToday, isAfter, addMinutes, startOfDay, isValid, parseISO };
|
package/demos/app.js
CHANGED
@@ -1,9 +1,16 @@
|
|
1
|
-
|
1
|
+
require('sucrase/register');
|
2
|
+
const nExpress = require('@financial-times/n-express');
|
2
3
|
const chalk = require('chalk');
|
3
4
|
const errorHighlight = chalk.bold.red;
|
4
5
|
const highlight = chalk.bold.green;
|
6
|
+
const { PageKitReactJSX } = require('@financial-times/dotcom-server-react-jsx');
|
7
|
+
const fs = require('fs');
|
8
|
+
const path = require('path');
|
9
|
+
const handlebars = require('handlebars');
|
10
|
+
const { PageKitHandlebars, helpers } = require('@financial-times/dotcom-server-handlebars');
|
5
11
|
|
6
|
-
const
|
12
|
+
const demoJSX = require('./templates/demo').default;
|
13
|
+
const demoLayoutSource = fs.readFileSync(path.join(__dirname, './templates/demo-layout.html'),'utf8').toString();
|
7
14
|
|
8
15
|
const fixtures = {
|
9
16
|
followButton: require('./fixtures/follow-button'),
|
@@ -14,48 +21,59 @@ const fixtures = {
|
|
14
21
|
instantAlert: require('./fixtures/instant-alert')
|
15
22
|
};
|
16
23
|
|
17
|
-
const app = module.exports =
|
24
|
+
const app = module.exports = nExpress({
|
18
25
|
name: 'public',
|
19
26
|
systemCode: 'n-myft-ui-demo',
|
20
27
|
withFlags: true,
|
21
|
-
|
22
|
-
|
28
|
+
withConsent: false,
|
29
|
+
withServiceMetrics: false,
|
23
30
|
withAnonMiddleware: false,
|
24
31
|
hasHeadCss: false,
|
25
|
-
layoutsDir: 'demos/templates',
|
26
|
-
viewsDirectory: '/demos/templates',
|
27
32
|
partialsDirectory: process.cwd(),
|
28
33
|
directory: process.cwd(),
|
29
34
|
demo: true,
|
30
|
-
|
31
|
-
helpers: {
|
32
|
-
x: xHandlebars()
|
33
|
-
}
|
35
|
+
withBackendAuthentication: false,
|
34
36
|
});
|
35
37
|
|
38
|
+
app.set('views', path.join(__dirname, '/templates'));
|
39
|
+
app.set('view engine', '.html');
|
40
|
+
|
41
|
+
app.engine('.html', new PageKitHandlebars({
|
42
|
+
cache: false,
|
43
|
+
handlebars,
|
44
|
+
helpers
|
45
|
+
}).engine);
|
46
|
+
|
47
|
+
app.use('/public', nExpress.static(path.join(__dirname, '../public'), { redirect: false }));
|
48
|
+
|
49
|
+
const jsxRenderer = (new PageKitReactJSX({ includeDoctype: false }));
|
50
|
+
|
36
51
|
app.get('/', (req, res) => {
|
37
52
|
res.render('demo', Object.assign({
|
38
53
|
title: 'n-myft-ui demo',
|
39
|
-
layout: 'demo-layout',
|
40
54
|
flags: {
|
41
55
|
myFtApi: true,
|
42
56
|
myFtApiWrite: true
|
43
|
-
}
|
57
|
+
},
|
44
58
|
}, fixtures));
|
45
59
|
});
|
46
60
|
|
47
|
-
app.get('/
|
48
|
-
|
49
|
-
title: 'n-myft-ui
|
50
|
-
layout: 'demo-layout',
|
61
|
+
app.get('/demo-jsx', async (req, res) => {
|
62
|
+
let demo = await jsxRenderer.render(demoJSX, Object.assign({
|
63
|
+
title: 'n-myft-ui demo',
|
51
64
|
flags: {
|
52
65
|
myFtApi: true,
|
53
|
-
myFtApiWrite: true
|
54
|
-
}
|
55
|
-
|
56
|
-
|
66
|
+
myFtApiWrite: true
|
67
|
+
}
|
68
|
+
}, fixtures));
|
69
|
+
|
70
|
+
let template = handlebars.compile(demoLayoutSource);
|
71
|
+
let result = template({body: demo});
|
72
|
+
|
73
|
+
res.send(result);
|
57
74
|
});
|
58
75
|
|
76
|
+
|
59
77
|
function runPa11yTests () {
|
60
78
|
const spawn = require('child_process').spawn;
|
61
79
|
const pa11y = spawn('pa11y-ci');
|