@financial-times/n-myft-ui 26.1.0 → 27.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. package/.circleci/config.yml +46 -34
  2. package/.nvmrc +1 -1
  3. package/Makefile +0 -1
  4. package/README.md +2 -48
  5. package/build-state/npm-shrinkwrap.json +10147 -20187
  6. package/components/collections/collections.html +85 -0
  7. package/components/concept-list/concept-list.html +31 -0
  8. package/components/csrf-token/input.html +5 -0
  9. package/components/follow-button/follow-button.html +79 -0
  10. package/components/instant-alert/instant-alert.html +47 -0
  11. package/components/pin-button/pin-button.html +20 -0
  12. package/components/save-for-later/save-for-later.html +67 -0
  13. package/components/unread-articles-indicator/date-fns.js +6 -6
  14. package/demos/app.js +3 -26
  15. package/demos/templates/demo.html +11 -10
  16. package/package.json +16 -30
  17. package/components/collections/collections.jsx +0 -68
  18. package/components/collections/collections.test.js +0 -83
  19. package/components/concept-list/concept-list.jsx +0 -69
  20. package/components/concept-list/concept-list.test.js +0 -116
  21. package/components/csrf-token/input.jsx +0 -20
  22. package/components/csrf-token/input.test.js +0 -23
  23. package/components/follow-button/follow-button.jsx +0 -176
  24. package/components/follow-button/follow-button.test.js +0 -40
  25. package/components/index.js +0 -17
  26. package/components/instant-alert/instant-alert.jsx +0 -73
  27. package/components/instant-alert/instant-alert.test.js +0 -86
  28. package/components/pin-button/pin-button.jsx +0 -40
  29. package/components/pin-button/pin-button.test.js +0 -57
  30. package/components/save-for-later/save-for-later.jsx +0 -101
  31. package/components/save-for-later/save-for-later.test.js +0 -59
  32. package/demos/templates/demo-layout.html +0 -25
  33. package/demos/templates/demo.jsx +0 -125
  34. package/dist/bundles/bundle.js +0 -3232
  35. package/jest.config.js +0 -8
  36. package/jsx-migration.md +0 -16
  37. package/webpack.config.js +0 -34
@@ -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
- }
@@ -1,57 +0,0 @@
1
- import React from 'react';
2
- import { render } from '@testing-library/react';
3
- import '@testing-library/jest-dom';
4
- import PinButton from './pin-button';
5
-
6
- const flags = {
7
- myFtApi: true,
8
- myFtApiWrite: true
9
- };
10
-
11
- const fixtures = [
12
- {
13
- id: '00000000-0000-0000-0000-000000000022',
14
- name: 'myFT Enterprises',
15
- directType: 'http://www.ft.com/ontology/Topic',
16
- showPrioritiseButton: true
17
- },
18
- {
19
- id: '00000000-0000-0000-0000-000000000023',
20
- name: 'myFT Enterprises',
21
- directType: 'http://www.ft.com/ontology/Topic',
22
- showPrioritiseButton: false
23
- }
24
- ];
25
-
26
-
27
- describe('Pin Button', () => {
28
-
29
- test('It renders', () => {
30
- const { container } = render(<PinButton flags={flags} {...fixtures[0]} />);
31
- expect(container.querySelector(`button[id="myft-pin-button__${fixtures[0].id}"]`)).toBeTruthy();
32
- expect(container.querySelector('button[data-trackable="prioritised"]')).toBeTruthy();
33
- });
34
-
35
- test('It renders unprioritised', () => {
36
- const { container } = render(<PinButton flags={flags} {...fixtures[0]} />);
37
- expect(container.querySelector('button[aria-label="Pin myFT Enterprises in my F T"]')).toBeTruthy();
38
- expect(container.querySelector('button[title="Pin myFT Enterprises"]')).toBeTruthy();
39
- expect(container.querySelector('button[data-prioritised=false]')).toBeTruthy();
40
- expect(container.querySelector(`button[data-concept-id="${fixtures[0].id}"]`)).toBeTruthy();
41
- });
42
-
43
- test('It renders with prioritised', () => {
44
- const { container } = render(<PinButton flags={flags} prioritised={true} {...fixtures[0]} />);
45
- expect(container.querySelector('button[aria-label="Unpin myFT Enterprises from my F T"]')).toBeTruthy();
46
- expect(container.querySelector('button[title="Unpin myFT Enterprises"]')).toBeTruthy();
47
- expect(container.querySelector('button[data-prioritised=true]')).toBeTruthy();
48
- expect(container.querySelector(`button[data-concept-id="${fixtures[0].id}"]`)).toBeTruthy();
49
- });
50
-
51
- test('It renders the form element', () => {
52
- const { container } = render(<PinButton flags={flags} {...fixtures[0]} />);
53
- expect(container.querySelector('form[method="post"]')).toBeTruthy();
54
- expect(container.querySelector(`form[action="/__myft/api/core/prioritised/concept/${fixtures[0].id}?method=put"]`)).toBeTruthy();
55
- });
56
-
57
- });
@@ -1,101 +0,0 @@
1
- import React, { Fragment } from 'react';
2
- import CsrfToken from '../csrf-token/input';
3
-
4
- const ButtonContent = ({ saveButtonWithIcon, buttonText, isSaved, appIsStreamPage }) => {
5
-
6
- const DefaultButtonText = () => {
7
- if (appIsStreamPage !== true) {
8
- return <Fragment>
9
- <span className="save-button-longer-copy" data-variant-label>
10
- {isSaved ? 'Saved ' : 'Save '}
11
- </span>
12
- <span className="n-myft-ui__button--viewport-large" aria-hidden="true">to myFT</span>
13
- </Fragment>
14
- }
15
-
16
- return <span>{isSaved ? 'Saved' : 'Save'}</span>;
17
- }
18
-
19
- return (<Fragment>
20
- {
21
- saveButtonWithIcon &&
22
- <span className="save-button-with-icon-copy" data-variant-label>
23
- {buttonText && buttonText}
24
- {!buttonText && (isSaved ? 'Saved' : 'Save')}
25
- </span>
26
- }
27
-
28
- {
29
- !saveButtonWithIcon &&
30
- <Fragment>
31
- {buttonText && buttonText}
32
- {!buttonText && <DefaultButtonText />
33
- }
34
- </Fragment>
35
- }
36
- </Fragment>);
37
- }
38
- export default function SaveForLater({ flags, contentId, title, variant, trackableId, isSaved, appIsStreamPage, alternateText, saveButtonWithIcon, buttonText, csrfToken, cacheablePersonalisedUrl }) {
39
-
40
- const { myFtApiWrite } = flags;
41
-
42
- const generateSubmitButtonProps = () => {
43
- let props = {
44
- type: 'submit',
45
- 'data-trackable': trackableId ? trackableId : 'save-for-later',
46
- 'data-text-variant': appIsStreamPage !== true ? 'save-button-with-icon-copy' : 'save-button-longer-copy',
47
- 'data-content-id': contentId,
48
- className: saveButtonWithIcon ? 'n-myft-ui__save-button-with-icon' : `n-myft-ui__button ${variant ? `n-myft-ui__button--${variant}` : ''}`
49
- };
50
-
51
- if (isSaved) {
52
- let titleText = `${title ? `${title} is` : ''} saved to myFT`;
53
- props['title'] = title;
54
- props['aria-label'] = titleText;
55
- props['data-alternate-label'] = title ? `Save ${title} to myFT for later` : 'Save this article to myFT for later';
56
- props['aria-pressed'] = true;
57
- } else {
58
- let titleText = title ? `Save ${title} to myFT for later` : 'Save this article to myFT for later';
59
- props['title'] = titleText;
60
- props['aria-label'] = titleText;
61
- props['data-alternate-label'] = `${title ? `${title} is` : ''} saved to myFT`;
62
- props['aria-pressed'] = false;
63
- }
64
-
65
- if (alternateText) {
66
- props['data-alternate-text'] = alternateText;
67
- } else if (isSaved) {
68
- props['data-alternate-text'] = 'Save ';
69
- } else {
70
- props['data-alternate-text'] = 'Saved ';
71
- }
72
-
73
- return props;
74
- }
75
-
76
-
77
- return (
78
- <Fragment>
79
- {myFtApiWrite &&
80
- <form className="n-myft-ui n-myft-ui--save" method="GET"
81
- data-content-id={contentId}
82
- data-myft-ui="saved"
83
- action={`/myft/save/${contentId}`}
84
- data-js-action={`/__myft/api/core/saved/content/${contentId}?method=put`}>
85
- <CsrfToken csrfToken={csrfToken} cacheablePersonalisedUrl={cacheablePersonalisedUrl} />
86
-
87
- <div
88
- className="n-myft-ui__announcement o-normalise-visually-hidden"
89
- aria-live="assertive"
90
- data-pressed-text="Article saved in My FT."
91
- data-unpressed-text="Removed article from My FT."
92
- ></div>
93
- <button {...generateSubmitButtonProps()}>
94
- <ButtonContent buttonText={buttonText} saveButtonWithIcon={saveButtonWithIcon} isSaved={isSaved} appIsStreamPage={appIsStreamPage} />
95
- </button>
96
- </form>
97
- }
98
- </Fragment>
99
-
100
- )
101
- }
@@ -1,59 +0,0 @@
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,25 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
-
4
- <head>
5
- <meta charset="utf-8">
6
- <title>{{title}}</title>
7
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
8
- <link rel="stylesheet" href="/public/main.css">
9
- </head>
10
-
11
- <body>
12
-
13
- <div class="o-grid-container o-grid-container--snappy">
14
- <div class="o-grid-row">
15
- <ul>
16
- <li><a href="/">Basic</a></li>
17
- <li><a href="/demo-jsx">JSX demo</a></li>
18
- </ul>
19
- </div>
20
- </div>
21
-
22
- {{{body}}}
23
- </body>
24
-
25
- </html>
@@ -1,125 +0,0 @@
1
- import React from 'react';
2
- import FollowButton from '../../components/follow-button/follow-button';
3
- import ConceptList from '../../components/concept-list/concept-list';
4
- import Collections from '../../components/collections/collections';
5
- import { SaveForLater } from '../../components';
6
- import { PinButton } from '../../components';
7
-
8
- export default function Demo (props) {
9
-
10
- const {
11
- title,
12
- flags,
13
- followButton,
14
- conceptList,
15
- collections,
16
- saveButton,
17
- pinButton
18
- } = props;
19
-
20
- const followButtonProps = { ...followButton, flags };
21
-
22
- return (
23
- <div className="o-grid-container o-grid-container--snappy demo-container">
24
- <h1>{title}</h1>
25
-
26
- <section
27
- id="follow-button"
28
- className="demo-section">
29
- <div className="o-grid-row">
30
- <div data-o-grid-colspan="12">
31
- <h2
32
- className="demo-section__title">
33
- Follow button
34
- </h2>
35
- <FollowButton {...followButtonProps} />
36
-
37
-
38
- <h2
39
- className="demo-section__title">
40
- x-dash follow button
41
- </h2>
42
-
43
- <FollowButton {...followButtonProps} buttonText={followButton.name} />
44
-
45
-
46
- <h2 className="demo-section__title">
47
- Save button
48
- </h2>
49
- <SaveForLater flags={flags} {...saveButton} />
50
-
51
- <h2 className="demo-section__title">
52
- Unsave button
53
- </h2>
54
- <SaveForLater flags={flags} {...saveButton} isSaved={true} />
55
-
56
- <h2 className="demo-section__title">
57
- Unsave button with icon
58
- </h2>
59
- <SaveForLater flags={flags} {...saveButton} saveButtonWithIcon={true} />
60
-
61
- <h2 className="demo-section__title">
62
- Save button with icon
63
- </h2>
64
- <SaveForLater flags={flags} {...saveButton} isSaved={true} saveButtonWithIcon={true} />
65
-
66
- <h2 className="demo-section__title">
67
- Pin button
68
- </h2>
69
-
70
- {pinButton.map((item, index) => <PinButton key={index} {...item}/>)}
71
-
72
- </div>
73
- </div>
74
- </section>
75
-
76
- <section
77
- id="topic-list"
78
- className="demo-section">
79
- <div className="o-grid-row">
80
- <div data-o-grid-colspan="12">
81
- <h2 className="demo-section__title">
82
- Topic list
83
- </h2>
84
-
85
- <p className="demo-section__description">
86
- A list of topics to follow
87
- </p>
88
- </div>
89
-
90
- {
91
- conceptList && conceptList.map((list, index) =>
92
- <div key={index} data-o-grid-colspan="3">
93
- <ConceptList {...list} flags={flags} />
94
- </div>)
95
- }
96
-
97
- </div>
98
- </section>
99
-
100
- <section
101
- id="collections"
102
- className="demo-section">
103
- <div className="o-grid-row">
104
- <div data-o-grid-colspan="12">
105
- <h2 className="demo-section__title">
106
- Collections
107
- </h2>
108
-
109
- <p className="demo-section__description">
110
- Curated collections of topics to follow.
111
- </p>
112
- </div>
113
-
114
- {collections.map((collection, index) => (
115
- <div key={index} data-o-grid-colspan="3">
116
- <Collections {...collection} flags={flags} />
117
- </div>
118
- ))}
119
-
120
- </div>
121
- </section>
122
-
123
- </div>
124
- )
125
- }