@financial-times/n-myft-ui 25.0.1 → 26.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (30) hide show
  1. package/.circleci/config.yml +3 -0
  2. package/README.md +50 -0
  3. package/build-state/npm-shrinkwrap.json +14847 -15637
  4. package/components/collections/collections.jsx +68 -0
  5. package/components/collections/collections.test.js +83 -0
  6. package/components/concept-list/concept-list.jsx +69 -0
  7. package/components/concept-list/concept-list.test.js +116 -0
  8. package/components/csrf-token/input.jsx +1 -7
  9. package/components/csrf-token/{__tests__/input.test.js → input.test.js} +2 -2
  10. package/components/follow-button/follow-button.jsx +6 -4
  11. package/components/follow-button/{__tests__/follow-button.test.js → follow-button.test.js} +4 -4
  12. package/components/index.js +17 -0
  13. package/components/instant-alert/instant-alert.jsx +73 -0
  14. package/components/instant-alert/instant-alert.test.js +86 -0
  15. package/components/pin-button/pin-button.jsx +40 -0
  16. package/components/pin-button/pin-button.test.js +57 -0
  17. package/components/save-for-later/save-for-later.jsx +101 -0
  18. package/components/save-for-later/save-for-later.test.js +59 -0
  19. package/demos/app.js +2 -4
  20. package/demos/templates/demo.html +8 -9
  21. package/demos/templates/demo.jsx +93 -1
  22. package/dist/bundles/bundle.js +3232 -0
  23. package/jsx-migration.md +16 -0
  24. package/package.json +7 -3
  25. package/webpack.config.js +34 -0
  26. package/components/collections/collections.html +0 -77
  27. package/components/concept-list/concept-list.html +0 -28
  28. package/components/instant-alert/instant-alert.html +0 -47
  29. package/components/pin-button/pin-button.html +0 -20
  30. 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 = [], csrfToken, cacheablePersonalisedUrl }) {
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 cacheablePersonalisedUrl={cacheablePersonalisedUrl} csrfToken={csrfToken} 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 csrfToken={csrfToken} cacheablePersonalisedUrl={cacheablePersonalisedUrl} />
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,69 @@
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
+ }
@@ -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
+ });
@@ -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 '../input';
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=\'test-token\']')).toBeTruthy();
19
+ expect(container.querySelector('[data-myft-csrf-token]')).toBeTruthy();
20
20
  });
21
21
 
22
22
 
@@ -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) {
@@ -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);
@@ -148,14 +150,14 @@ export default function FollowButton (props) {
148
150
  const getVariantClass = (variant) => variant ? `n-myft-follow-button--${variant}` : '';
149
151
 
150
152
  return (
151
- <>
153
+ <Fragment>
152
154
  {flags.myFtApiWrite && <form
153
155
  className={`n-myft-ui n-myft-ui--follow ${extraClasses || ''}`}
154
156
  method="GET"
155
157
  data-myft-ui="follow"
156
158
  data-concept-id={conceptId}
157
159
  {...formProps}>
158
- <CsrfToken cacheablePersonalisedUrl={props.cacheablePersonalisedUrl} csrfToken={props.csrfToken} />
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"
@@ -168,7 +170,7 @@ export default function FollowButton (props) {
168
170
  {getButtonText(props)}
169
171
  </button>
170
172
  </form>}
171
- </>
173
+ </Fragment>
172
174
  );
173
175
 
174
176
  }
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import FollowButton from '../follow-button';
2
+ import FollowButton from './follow-button';
3
3
  import { render, screen } from '@testing-library/react';
4
4
  import '@testing-library/jest-dom';
5
5
 
@@ -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
  });
@@ -0,0 +1,17 @@
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
+ };
@@ -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
+ });
@@ -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, 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
+ }