@financial-times/n-myft-ui 23.1.3 → 24.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -10,17 +10,9 @@
10
10
  {{#concepts}}
11
11
  <li class="collection__concept">
12
12
  {{#if ../liteStyle}}
13
- {{> n-myft-ui/components/follow-button/follow-button
14
- variant="primary"
15
- buttonText=name
16
- collectionName=../collectionName
17
- }}
13
+ {{{renderReactComponent localPath="components/follow-button/follow-button" variant="primary" buttonText=name flags=@root.flags collectionName=../collectionName}}}
18
14
  {{else}}
19
- {{> n-myft-ui/components/follow-button/follow-button
20
- variant="inverse"
21
- buttonText=name
22
- collectionName=../collectionName
23
- }}
15
+ {{{renderReactComponent localPath="components/follow-button/follow-button" variant="inverse" buttonText=name flags=@root.flags collectionName=../collectionName}}}
24
16
  {{/if}}
25
17
  </li>
26
18
  {{/concepts}}
@@ -50,7 +42,7 @@
50
42
  {{~/unless~}}
51
43
  {{~/concepts~}}"
52
44
  />
53
- {{> n-myft-ui/components/csrf-token/input}}
45
+ {{{renderReactComponent localPath="components/csrf-token/input"}}}
54
46
  <input
55
47
  type="hidden"
56
48
  name="name"
@@ -20,10 +20,7 @@
20
20
  class="concept-list__concept">
21
21
  {{prefLabel}}
22
22
  </a>
23
- {{> n-myft-ui/components/follow-button/follow-button
24
- conceptId=id
25
- name=prefLabel
26
- }}
23
+ {{{renderReactComponent localPath="components/follow-button/follow-button" conceptId=id name=prefLabel flags=@root.flags}}}
27
24
  </li>
28
25
  {{/each}}
29
26
  </ul>
@@ -0,0 +1,23 @@
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=\'test-token\']')).toBeTruthy();
20
+ });
21
+
22
+
23
+ });
@@ -0,0 +1,26 @@
1
+ import React from 'react';
2
+
3
+ export default function CsrfToken ({ cacheablePersonalisedUrl, csrfToken }) {
4
+
5
+ let inputProps = {};
6
+
7
+ if (cacheablePersonalisedUrl) {
8
+ inputProps = {
9
+ ...inputProps,
10
+ 'data-myft-csrf-token': csrfToken
11
+ };
12
+ }
13
+
14
+ if(csrfToken) {
15
+ inputProps.value = csrfToken;
16
+ }
17
+
18
+ return (
19
+ <input
20
+ {...inputProps}
21
+ type="hidden"
22
+ name="token"
23
+ />
24
+ );
25
+
26
+ }
@@ -0,0 +1,40 @@
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(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', () => {
33
+ render(<FollowButton {...props}
34
+ variant={'standard'}
35
+ setFollowButtonStateToSelected={true}
36
+ cacheablePersonalisedUrl={true} />);
37
+ expect(screen.findByText('Added')).toBeTruthy();
38
+ });
39
+
40
+ });
@@ -0,0 +1,174 @@
1
+ import React 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
+ } = props;
144
+
145
+ const formProps = generateFormProps(props);
146
+ const buttonProps = generateButtonProps(props);
147
+
148
+ const getVariantClass = (variant) => variant ? `n-myft-follow-button--${variant}` : '';
149
+
150
+ return (
151
+ <>
152
+ {flags.myFtApiWrite && <form
153
+ className={`n-myft-ui n-myft-ui--follow ${extraClasses || ''}`}
154
+ method="GET"
155
+ data-myft-ui="follow"
156
+ data-concept-id={conceptId}
157
+ {...formProps}>
158
+ <CsrfToken cacheablePersonalisedUrl={props.cacheablePersonalisedUrl} csrfToken={props.csrfToken} />
159
+ <div
160
+ className="n-myft-ui__announcement o-normalise-visually-hidden"
161
+ aria-live="assertive"
162
+ data-pressed-text={`Now following ${name}.`}
163
+ data-unpressed-text={`No longer following ${name}.`}
164
+ ></div>
165
+ <button
166
+ {...buttonProps}
167
+ className={[`n-myft-follow-button ${getVariantClass(variant)}`]}>
168
+ {getButtonText(props)}
169
+ </button>
170
+ </form>}
171
+ </>
172
+ );
173
+
174
+ }
@@ -5,7 +5,7 @@
5
5
  data-concept-id="{{conceptId}}"
6
6
  action="/myft/add/{{conceptId}}?instant=true"
7
7
  data-js-action="/__myft/api/core/followed/concept/{{conceptId}}?method=put">
8
- {{> n-myft-ui/components/csrf-token/input}}
8
+ {{{renderReactComponent localPath="components/csrf-token/input"}}}
9
9
  <input type="hidden" value="{{name}}" name="name">
10
10
  {{#if directType}}
11
11
  <input type="hidden" value="{{directType}}" name="directType">
@@ -2,7 +2,7 @@
2
2
  <span class="myft-pin-divider"></span>
3
3
  <div class="myft-pin-button-wrapper">
4
4
  <form method="post" action="/__myft/api/core/prioritised/concept/{{id}}?method={{#if prioritised}}delete{{else}}put{{/if}}" data-myft-prioritise>
5
- {{> n-myft-ui/components/csrf-token/input }}
5
+ {{{renderReactComponent localPath="components/csrf-token/input"}}}
6
6
  <input type="hidden" value="{{name}}" name="name"> {{#if directType}}
7
7
  <input type="hidden" value="{{directType}}" name="directType"> {{else}}
8
8
  <input type="hidden" value="http://www.ft.com/ontology/concept/Concept" name="directType"> {{/if}}
@@ -4,7 +4,7 @@
4
4
  data-myft-ui="saved"
5
5
  action="/myft/save/{{contentId}}"
6
6
  data-js-action="/__myft/api/core/saved/content/{{contentId}}?method=put">
7
- {{> n-myft-ui/components/csrf-token/input}}
7
+ {{{renderReactComponent localPath="components/csrf-token/input"}}}
8
8
  <div
9
9
  class="n-myft-ui__announcement o-normalise-visually-hidden"
10
10
  aria-live="assertive"
@@ -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/master/CHANGELOG.md#200---2019-08-20
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('Invalid date passed', [date]); //eslint-disable-line
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) => isAfterOriginal(isValid(date), isValid(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,17 @@
1
+ require('sucrase/register');
1
2
  const express = require('@financial-times/n-internal-tool');
2
3
  const chalk = require('chalk');
3
4
  const errorHighlight = chalk.bold.red;
4
5
  const highlight = chalk.bold.green;
5
-
6
+ const { PageKitReactJSX } = require('@financial-times/dotcom-server-react-jsx');
7
+ const fs = require('fs');
8
+ const path = require('path');
6
9
  const xHandlebars = require('@financial-times/x-handlebars');
10
+ const handlebars = require('handlebars');
11
+ const { helpers } = require('@financial-times/dotcom-server-handlebars');
12
+
13
+ const demoJSX = require('./templates/demo').default;
14
+ const demoLayoutSource = fs.readFileSync(path.join(__dirname, './templates/demo-layout.html'),'utf8').toString();
7
15
 
8
16
  const fixtures = {
9
17
  followButton: require('./fixtures/follow-button'),
@@ -29,10 +37,13 @@ const app = module.exports = express({
29
37
  demo: true,
30
38
  s3o: false,
31
39
  helpers: {
32
- x: xHandlebars()
33
- }
40
+ x: xHandlebars(),
41
+ renderReactComponent: helpers.renderReactComponent
42
+ },
34
43
  });
35
44
 
45
+ const jsxRenderer = (new PageKitReactJSX({ includeDoctype: false }));
46
+
36
47
  app.get('/', (req, res) => {
37
48
  res.render('demo', Object.assign({
38
49
  title: 'n-myft-ui demo',
@@ -44,6 +55,22 @@ app.get('/', (req, res) => {
44
55
  }, fixtures));
45
56
  });
46
57
 
58
+ app.get('/demo-jsx', async (req, res) => {
59
+ let demo = await jsxRenderer.render(demoJSX, Object.assign({
60
+ title: 'n-myft-ui demo',
61
+ layout: 'demo-layout',
62
+ flags: {
63
+ myFtApi: true,
64
+ myFtApiWrite: true
65
+ }
66
+ }, fixtures));
67
+
68
+ let template = handlebars.compile(demoLayoutSource);
69
+ let result = template({body: demo});
70
+
71
+ res.send(result);
72
+ });
73
+
47
74
  app.get('/digest-on-follow', (req, res) => {
48
75
  res.render('digest-on-follow', Object.assign({
49
76
  title: 'n-myft-ui digest on follow',
@@ -12,7 +12,7 @@
12
12
  </h2>
13
13
 
14
14
  {{#followButton}}
15
- {{> n-myft-ui/components/follow-button/follow-button}}
15
+ {{{renderReactComponent localPath="components/follow-button/follow-button" flags=@root.flags}}}
16
16
  {{/followButton}}
17
17
 
18
18
  <h2
@@ -21,9 +21,7 @@
21
21
  </h2>
22
22
 
23
23
  {{#followButton}}
24
- {{> n-myft-ui/components/follow-button/follow-button
25
- buttonText=name
26
- }}
24
+ {{{renderReactComponent localPath="components/follow-button/follow-button" buttonText=name flags=@root.flags}}}
27
25
  {{/followButton}}
28
26
 
29
27
  {{#saveButton}}
@@ -0,0 +1,33 @@
1
+ import React from 'react';
2
+ import FollowButton from '../../components/follow-button/follow-button';
3
+
4
+ export default function Demo (props) {
5
+
6
+ const {
7
+ title,
8
+ flags,
9
+ followButton,
10
+ } = props;
11
+
12
+ const followButtonProps = {...followButton, flags};
13
+
14
+ return (
15
+ <div className="o-grid-container o-grid-container--snappy demo-container">
16
+ <h1>{title}</h1>
17
+
18
+ <section
19
+ id="follow-button"
20
+ className="demo-section">
21
+ <div className="o-grid-row">
22
+ <div data-o-grid-colspan="12">
23
+ <h2
24
+ className="demo-section__title">
25
+ Follow button
26
+ </h2>
27
+ <FollowButton {...followButtonProps} />
28
+ </div>
29
+ </div>
30
+ </section>
31
+ </div>
32
+ )
33
+ }
@@ -5,7 +5,7 @@
5
5
  <h2>Follow button</h2>
6
6
 
7
7
  {{#followButton}}
8
- {{> n-myft-ui/components/follow-button/follow-button}}
8
+ {{{renderReactComponent localPath="components/follow-button/follow-button" flags=@root.flags}}}
9
9
  {{/followButton}}
10
10
  </div>
11
11
  </div>
package/jest.config.js ADDED
@@ -0,0 +1,8 @@
1
+ module.exports = {
2
+ transform: {
3
+ '.(js|jsx)': '@sucrase/jest-plugin',
4
+ },
5
+ testEnvironment: 'jest-environment-jsdom',
6
+ modulePathIgnorePatterns: ['node_modules', 'bower_components'],
7
+ testMatch: ['<rootDir>/components/**/*.test.js']
8
+ };
package/package.json CHANGED
@@ -1,14 +1,11 @@
1
1
  {
2
2
  "name": "@financial-times/n-myft-ui",
3
- "version": "23.1.3",
3
+ "version": "24.0.0",
4
4
  "description": "Client side component for interaction with myft",
5
5
  "main": "server.js",
6
6
  "scripts": {
7
7
  "test": "echo \"Error: no test specified\" && exit 1",
8
8
  "commit": "commit-wizard",
9
- "precommit": "node_modules/.bin/secret-squirrel",
10
- "prepush": "make verify -j3",
11
- "commitmsg": "node_modules/.bin/secret-squirrel-commitmsg",
12
9
  "prepare": "npx snyk protect || npx snyk protect -d || true"
13
10
  },
14
11
  "repository": {
@@ -27,10 +24,15 @@
27
24
  "@financial-times/dotcom-build-js": "0.4.1",
28
25
  "@financial-times/dotcom-build-sass": "0.4.1",
29
26
  "@financial-times/dotcom-page-kit-cli": "0.4.1",
30
- "@financial-times/n-gage": "^3.12.0",
27
+ "@financial-times/dotcom-server-handlebars": "^3.0.0",
28
+ "@financial-times/dotcom-server-react-jsx": "^2.6.2",
29
+ "@financial-times/n-gage": "^8.3.0",
31
30
  "@financial-times/n-heroku-tools": "8.3.1",
32
31
  "@financial-times/n-internal-tool": "2.3.4",
33
32
  "@financial-times/x-handlebars": "1.0.0-beta.21",
33
+ "@sucrase/jest-plugin": "^2.2.0",
34
+ "@testing-library/jest-dom": "^5.16.1",
35
+ "@testing-library/react": "^12.1.2",
34
36
  "ascii-table": "0.0.9",
35
37
  "autoprefixer": "9.7.0",
36
38
  "aws-sdk-mock": "4.5.0",
@@ -53,6 +55,7 @@
53
55
  "css-loader": "^0.23.1",
54
56
  "denodeify": "^1.2.1",
55
57
  "eslint": "6.5.1",
58
+ "eslint-plugin-react": "^7.27.1",
56
59
  "extract-css-block-webpack-plugin": "^1.3.0",
57
60
  "extract-text-webpack-plugin": "3.0.2",
58
61
  "fetch-mock": "^5.0.3",
@@ -62,6 +65,8 @@
62
65
  "hyperons": "^0.4.1",
63
66
  "imports-loader": "0.8.0",
64
67
  "inject-loader": "^3.0.0",
68
+ "jest": "^27.4.5",
69
+ "jsdom": "^19.0.0",
65
70
  "karma": "4.4.1",
66
71
  "karma-browserstack-launcher": "1.5.1",
67
72
  "karma-chai": "^0.1.0",
@@ -83,16 +88,25 @@
83
88
  "npm-prepublish": "^1.2.1",
84
89
  "pa11y-ci": "^2.1.1",
85
90
  "postcss-loader": "^0.9.1",
91
+ "react": "^17.0.2",
86
92
  "regenerator-runtime": "^0.13.3",
87
93
  "sass-loader": "^3.2.0",
88
94
  "semver": "6.3.0",
89
95
  "sinon": "^7.1.0",
90
96
  "sinon-chai": "^3.2.0",
91
- "snyk": "^1.216.5"
97
+ "snyk": "^1.216.5",
98
+ "sucrase": "^3.10.1"
92
99
  },
93
100
  "x-dash": {
94
101
  "engine": {
95
102
  "server": "hyperons"
96
103
  }
104
+ },
105
+ "husky": {
106
+ "hooks": {
107
+ "commit-msg": "node_modules/.bin/secret-squirrel-commitmsg",
108
+ "pre-commit": "node_modules/.bin/secret-squirrel",
109
+ "pre-push": "make verify -j3"
110
+ }
97
111
  }
98
112
  }
@@ -1,5 +0,0 @@
1
- <input
2
- data-myft-csrf-token
3
- value="{{#if @root.cacheablePersonalisedUrl}}{{@root.csrfToken}}{{/if}}"
4
- type="hidden"
5
- name="token">
@@ -1,79 +0,0 @@
1
- {{#if @root.flags.myFtApiWrite}}
2
- <form
3
- class="n-myft-ui n-myft-ui--follow {{extraClasses}}"
4
- method="GET"
5
- data-myft-ui="follow"
6
- data-concept-id="{{conceptId}}"
7
- {{#if collectionName}}data-myft-tracking="collectionName={{collectionName}}"{{/if}}
8
- {{#if followPlusDigestEmail}}
9
- action="/__myft/api/core/follow-plus-digest-email/{{conceptId}}?method=put"
10
- data-myft-ui-variant="followPlusDigestEmail"
11
- {{else}}
12
- {{#ifAll setFollowButtonStateToSelected @root.cacheablePersonalisedUrl}}
13
- action="/myft/remove/{{conceptId}}"
14
- data-js-action="/__myft/api/core/followed/concept/{{conceptId}}?method=delete"
15
- {{else}}
16
- action="/myft/add/{{conceptId}}"
17
- data-js-action="/__myft/api/core/followed/concept/{{conceptId}}?method=put"
18
- {{/ifAll}}
19
- {{/if}}>
20
- {{> n-myft-ui/components/csrf-token/input}}
21
- <div
22
- class="n-myft-ui__announcement o-normalise-visually-hidden"
23
- aria-live="assertive"
24
- data-pressed-text="Now following {{name}}."
25
- data-unpressed-text="No longer following {{name}}."
26
- ></div>
27
- <button
28
- {{#ifAll setFollowButtonStateToSelected @root.cacheablePersonalisedUrl}}
29
- aria-label="Remove {{name}} from myFT"
30
- title="Remove {{name}} from myFT"
31
- data-alternate-label="Add {{name}} to myFT"
32
- aria-pressed="true"
33
- {{#if alternateText}}
34
- data-alternate-text="{{alternateText}}"
35
- {{else}}
36
- {{#if buttonText}}
37
- data-alternate-text="{{buttonText}}"
38
- {{else}}
39
- data-alternate-text="Add to myFT"
40
- {{/if}}
41
- {{/if}}
42
- {{else}}
43
- aria-pressed="false"
44
- aria-label="Add {{name}} to myFT"
45
- title="Add {{name}} to myFT"
46
- data-alternate-label="Remove {{name}} from myFT"
47
- {{#if alternateText}}
48
- data-alternate-text="{{alternateText}}"
49
- {{else}}
50
- {{#if buttonText}}
51
- data-alternate-text="{{buttonText}}"
52
- {{else}}
53
- data-alternate-text="Added"
54
- {{/if}}
55
- {{/if}}
56
- {{/ifAll}}
57
- class="{{extraButtonClasses}}
58
- n-myft-follow-button
59
- {{~#variant}} n-myft-follow-button--{{this}}{{/variant~}}"
60
- data-concept-id="{{conceptId}}" {{! duplicated here for tracking}}
61
- {{#if followPlusDigestEmail}}
62
- data-trackable-context-messaging="add-to-myft-plus-digest-button"
63
- {{/if}}
64
- data-trackable="follow"
65
- type="submit">
66
- {{~#if buttonText~}}
67
- {{buttonText}}
68
- {{~else~}}
69
- {{~#ifAll setFollowButtonStateToSelected @root.cacheablePersonalisedUrl~}}
70
- Added
71
- {{~else~}}
72
- Add to myFT
73
- {{~/ifAll~}}
74
- {{~/if~}}
75
- </button>
76
- </form>
77
- {{else}}
78
- <!-- Add to myFT button hidden due to myFtApiWrite being off -->
79
- {{/if}}