Skip to content

Commit

Permalink
Merge pull request #59 from Financial-Times/follow-button
Browse files Browse the repository at this point in the history
x-follow-button
  • Loading branch information
edds authored Dec 4, 2019
2 parents 31a702c + f7474c7 commit 619d52e
Show file tree
Hide file tree
Showing 20 changed files with 596 additions and 7 deletions.
2 changes: 1 addition & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
**/public/**
**/public-prod/**
**/blueprints/**
web/static/**
web/static/**
1 change: 1 addition & 0 deletions .storybook/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ configure(() => {

// Add regular story definitions (i.e. those using storiesOf() directly below)
require('../components/x-increment/storybook/index.jsx');
require('../components/x-follow-button/storybook/index.jsx');
}, module);
2 changes: 1 addition & 1 deletion .storybook/register-components.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const components = [
require('../components/x-teaser/storybook'),
require('../components/x-styling-demo/stories'),
require('../components/x-gift-article/stories'),
require('../components/x-podcast-launchers/stories'),
require('../components/x-podcast-launchers/stories')
];

module.exports = components;
24 changes: 24 additions & 0 deletions .storybook/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// See https://storybook.js.org/configurations/custom-webpack-config/ for more info.

const path = require('path');
const glob = require('glob');
const fs = require('fs');
const xBabelConfig = require('../packages/x-babel-config');
const xEngine = require('../packages/x-engine/src/webpack');
Expand Down Expand Up @@ -58,6 +59,29 @@ module.exports = ({ config }) => {
return name.includes('babel-preset-minify') === false;
});

config.module.rules.push({
test: /\.(scss|sass)$/,
use: [
{
loader: require.resolve('style-loader')
},
{
loader: require.resolve('css-loader'),
options: {
url: false,
import: false,
modules: true
}
},
{
loader: require.resolve('sass-loader'),
options: {
includePaths: glob.sync('./components/*/bower_components', { absolute: true }),
}
}
]
});

// HACK: Ensure we only bundle one instance of React
config.resolve.alias.react = require.resolve('react');

Expand Down
1 change: 1 addition & 0 deletions __mocks__/styleMock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = {};
8 changes: 8 additions & 0 deletions components/x-follow-button/.bowerrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"registry": {
"search": [
"https://origami-bower-registry.ft.com",
"https://registry.bower.io"
]
}
}
174 changes: 174 additions & 0 deletions components/x-follow-button/__tests__/x-follow-button.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
const { h } = require('@financial-times/x-engine');
const { mount } = require('@financial-times/x-test-utils/enzyme');

import { FollowButton } from '../src/FollowButton';

describe('x-follow-button', () => {
describe('concept name', () => {
it('when conceptNameAsButtonText prop is true, and the topic name is provided, the button is named by this name', () => {
const subject = mount(
<FollowButton conceptNameAsButtonText={true} conceptName={'dummy concept name'} />
);
expect(subject.find('button').text()).toEqual('dummy concept name');
});

it('when conceptNameAsButtonText prop is false, the button has a default name', () => {
const subject = mount(
<FollowButton conceptNameAsButtonText={false} conceptName={'dummy concept name'} />
);
expect(subject.find('button').text()).toEqual('Add to myFT');
});

it('when conceptNameAsButtonText prop is true, and the topic name is not provided, the button has a default name', () => {
const subject = mount(<FollowButton conceptNameAsButtonText={true} />);
expect(subject.find('button').text()).toEqual('Add to myFT');
});

it('when conceptNameAsButtonText prop is not provided, the button has a default name', () => {
const subject = mount(<FollowButton />);
expect(subject.find('button').text()).toEqual('Add to myFT');
});
});
describe('conceptId prop', () => {
it('assigns conceptId prop to data-concept-id attribute of the button', async () => {
const subject = mount(<FollowButton conceptId={'dummy-id'} />);
expect(subject.find('button').prop('data-concept-id')).toEqual('dummy-id');
});

it('assigns conceptId prop to data-concept-id attribute of the form', async () => {
const subject = mount(<FollowButton conceptId={'dummy-id'} />);
expect(subject.find('form').prop('data-concept-id')).toEqual('dummy-id');
});
});

describe('form action', () => {
it('assigns follow-plus-digest-email put action if followPlusDigestEmail is true', async () => {
const subject = mount(<FollowButton followPlusDigestEmail={true} conceptId={'dummy-id'} />);
expect(subject.find('form').prop('action')).toEqual(
'/__myft/api/core/follow-plus-digest-email/dummy-id?method=put'
);
});

it('assigns followed/concept delete action if isFollowed is true', async () => {
const subject = mount(<FollowButton isFollowed={true} conceptId={'dummy-id'} />);
expect(subject.find('form').prop('action')).toEqual(
'/__myft/api/core/followed/concept/dummy-id?method=delete'
);
});

it('assigns followed/concept put action if isFollowed and followPlusDigestEmail are not passed', async () => {
const subject = mount(<FollowButton conceptId={'dummy-id'} />);
expect(subject.find('form').prop('action')).toEqual(
'/__myft/api/core/followed/concept/dummy-id?method=put'
);
});
});

describe('isFollowed', () => {
describe('when true', () => {
it('button text is "Added"', () => {
const subject = mount(<FollowButton isFollowed={true} />);
expect(subject.find('button').text()).toEqual('Added');
});

it('button aria-pressed is "true"', () => {
const subject = mount(<FollowButton isFollowed={true} />);
expect(subject.find('button').prop('aria-pressed')).toEqual('true');
});

it('button title is "Remove ConceptName from myFT"', () => {
const subject = mount(<FollowButton isFollowed={true} conceptName={'ConceptName'} />);
expect(subject.find('button').prop('title')).toEqual('Remove ConceptName from myFT');
});

it('button aria-label is "Remove conceptName from myFT"', () => {
const subject = mount(<FollowButton isFollowed={true} conceptName={'ConceptName'} />);
expect(subject.find('button').prop('aria-label')).toEqual('Remove ConceptName from myFT');
});
});

describe('when false', () => {
it('button text is "Add to myFT"', () => {
const subject = mount(<FollowButton isFollowed={false} />);
expect(subject.find('button').text()).toEqual('Add to myFT');
});

it('button aria-pressed is "false"', () => {
const subject = mount(<FollowButton isFollowed={false} />);
expect(subject.find('button').prop('aria-pressed')).toEqual('false');
});

it('button title is "Add ConceptName to myFT"', () => {
const subject = mount(<FollowButton isFollowed={false} conceptName={'ConceptName'} />);
expect(subject.find('button').prop('title')).toEqual('Add ConceptName to myFT');
});

it('button aria-label is "Add ConceptName to myFT"', () => {
const subject = mount(<FollowButton isFollowed={false} conceptName={'ConceptName'} />);
expect(subject.find('button').prop('aria-label')).toEqual('Add ConceptName to myFT');
});
});
});

describe('followPlusDigestEmail', () => {
describe('when true', () => {
it('form has data-myft-ui-variant property which is true', () => {
const subject = mount(<FollowButton followPlusDigestEmail={true} />);
expect(subject.find('form').prop('data-myft-ui-variant')).toEqual(true);
});

it('button has data-trackable-context-messaging property which is add-to-myft-plus-digest-button', () => {
const subject = mount(<FollowButton followPlusDigestEmail={true} />);
expect(subject.find('button').prop('data-trackable-context-messaging')).toEqual(
'add-to-myft-plus-digest-button'
);
});
});

describe('when false', () => {
it('form has data-myft-ui-variant property which is true', () => {
const subject = mount(<FollowButton followPlusDigestEmail={false} />);
expect(subject.find('form').prop('data-myft-ui-variant')).toEqual(undefined);
});

it('button has data-trackable-context-messaging property which is add-to-myft-plus-digest-button', () => {
const subject = mount(<FollowButton followPlusDigestEmail={false} />);
expect(subject.find('button').prop('data-trackable-context-messaging')).toEqual(null);
});
});
});

describe('form properties', () => {
it('method = GET', () => {
const subject = mount(<FollowButton />);
expect(subject.find('form').prop('method')).toEqual('GET');
});
});

describe('button properties', () => {
it('data-trackable="follow"', () => {
const subject = mount(<FollowButton />);
expect(subject.find('button').prop('data-trackable')).toEqual('follow');
});

it('type="submit"', () => {
const subject = mount(<FollowButton />);
expect(subject.find('button').prop('type')).toEqual('submit');
});
});

describe('csrf token', () => {
it('if passed creates an invisible input field', () => {
const subject = mount(<FollowButton csrfToken={'dummyToken'} />);
expect(subject.find('input').prop('value')).toEqual('dummyToken');
expect(subject.find('input').prop('type')).toEqual('hidden');
expect(subject.find('input').prop('name')).toEqual('token');
expect(subject.find('input').prop('data-myft-csrf-token')).toEqual(true);
});

it('if not passed an invisible input field is not created', () => {
const subject = mount(<FollowButton csrf={'dummyToken'} />);
expect(subject.find('input')).toEqual({});
});
});
});
10 changes: 10 additions & 0 deletions components/x-follow-button/bower.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "x-follow-button",
"private": true,
"main": "dist/FollowButton.es5.js",
"dependencies": {
"o-colors": "^5.0.3",
"o-icons": "^6.0.0",
"o-typography": "^6.1.0"
}
}
31 changes: 31 additions & 0 deletions components/x-follow-button/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "@financial-times/x-follow-button",
"version": "1.0.0",
"description": "",
"main": "dist/FollowButton.cjs.js",
"style": "dist/FollowButton.css",
"browser": "dist/FollowButton.es5.js",
"module": "dist/FollowButton.esm.js",
"scripts": {
"prepare": "bower install && npm run build",
"build": "node rollup.js",
"start": "node rollup.js --watch"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@financial-times/x-rollup": "file:../../packages/x-rollup",
"@financial-times/x-test-utils": "file:../../packages/x-test-utils",
"rollup": "^0.57.1",
"bower": "^1.7.9",
"node-sass": "^4.9.2"
},
"peerDependencies": {
"@financial-times/x-engine": "file:../../packages/x-engine"
},
"dependencies": {
"@financial-times/x-engine": "file:../../packages/x-engine",
"classnames": "^2.2.6"
}
}
43 changes: 43 additions & 0 deletions components/x-follow-button/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# x-follow-button

This module provides a template for myFT follow topic button, and is intended to replace the legacy handlebars component in [n-myft-ui](https://github.com/Financial-Times/n-myft-ui/tree/master/components/follow-button).

## Installation

```bash
npm install --save @financial-times/x-follow-button
```

## Props

(Some of the properties don't influence the way button looks or acts, but can be used for e.g. client-side Javascript in the apps).

Feature | Type | Required | Default value | Description
----------------------------|---------|----------|----------------|---------------
`conceptId` | String | yes | none | UUID of the concept
`conceptName` | String | yes | none | Name of the concept
`conceptNameAsButtonText` | Boolean | no | `false` | If true will use the concept name as the button text, otherwise will default to "Add to MyFT" or "Remove from MyFT" (depending on isFollowed prop).
`isFollowed` | Boolean | no | `false` | Whether the concept is followed or not.
`csrfToken` | String | no | none | CSRF token (will be included in a hidden form field).
`variant` | String | no | `standard` | One of `standard`, `inverse`, `opinion` or `monochrome`. Other values will be ignored.
`followPlusDigestEmail` | Boolean | no | `false` | Whether following the topic should also subscribe to the digest.

## Client side behaviour

For users with JavaScript enabled, the default form submit action is prevented, and a custom event (named 'x-follow-button') will be dispatched on the form element.

This custom event will contain the following in its `detail` object:

Property | Value
-------------------|-----------------
`action` | `add` or `remove`
`actorType` | `user`
`relationshipName` | `followed`
`subjectType` | `concept`
`subjectId` | the value of the `conceptId` prop
`token` | the value of the `csrfToken` prop

It is up to the consumer of this component to listen for the `x-follow-button` event, and use this data, along with the user's ID, and carry out the appropriate action.

For example, if using `next-myft-client` to carry out the follow/unfollow action, n-myft-ui provides a x-button-interaction component for this:
https://github.com/Financial-Times/n-myft-ui/blob/master/components/x-button-integration/index.js
4 changes: 4 additions & 0 deletions components/x-follow-button/rollup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const xRollup = require('@financial-times/x-rollup');
const pkg = require('./package.json');

xRollup({ input: './src/FollowButton.jsx', pkg });
Loading

0 comments on commit 619d52e

Please sign in to comment.