├── .babelrc
├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── __tests__
├── action.js
├── connectHoverable.jsx
├── index.js
└── reducer.js
├── package.json
└── src
├── action.js
├── connectHoverable.js
├── index.js
└── reducer.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "react", "stage-0"]
3 | }
4 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | coverage
2 | lib
3 | node_modules
4 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "airbnb",
3 | "parser": "babel-eslint",
4 | "plugins": [
5 | "react"
6 | ],
7 | "env": {
8 | "jest": true
9 | },
10 | "rules": {
11 | "consistent-return": 0,
12 | "func-names": 0,
13 | "no-param-reassign": 0,
14 | "no-underscore-dangle": 0,
15 | "space-before-function-paren": 0,
16 | "react/require-default-props": 0,
17 | "import/no-extraneous-dependencies": 0
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | coverage
3 | npm-debug.log
4 | lib
5 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "7"
4 | - "6"
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Buffer
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # buffer-redux-hover
2 |
3 | [](https://travis-ci.org/bufferapp/buffer-redux-hover)
4 |
5 | Keep react component hover state in redux
6 |
7 | ## Usage
8 |
9 | Combine the reducer under the `hover` state tree:
10 |
11 | ```js
12 | import { combineReducers } from 'redux';
13 | import { reducer as hover } from '@bufferapp/redux-hover';
14 |
15 | const app = combineReducers({
16 | hover, // important to have this under the hover state tree
17 | });
18 |
19 | export default app;
20 | ```
21 |
22 | Create a component that has a `hovered`, `onMouseEnter` and `onMouseLeave` prop:
23 |
24 | ```js
25 | import React from 'react';
26 | import { connectHoverable } from '@bufferapp/redux-hover';
27 |
28 | const MyHoverableComponent = ({
29 | hovered, // managed by redux-hover
30 | onMouseEnter,
31 | onMouseLeave,
32 | }) => {
33 | const style = {
34 | background: hovered ? 'red' : 'blue',
35 | };
36 | return (
37 |
43 | Hover This
44 |
45 | );
46 | };
47 |
48 | MyHoverableComponent.propTypes = {
49 | hoverId: PropTypes.oneOfType([
50 | PropTypes.string,
51 | PropTypes.number,
52 | ]),
53 | hovered: PropTypes.bool,
54 | onMouseEnter: PropTypes.func,
55 | onMouseLeave: PropTypes.func,
56 | };
57 |
58 | export default connectHoverable(MyHoverableComponent);
59 | ```
60 |
61 | Place `MyHoverableComponent` on the page and set a `hoverId`:
62 |
63 | ```js
64 | import React from 'react';
65 | import MyHoverableComponent from './MyHoverableComponent';
66 |
67 | const App = () =>
68 |
69 |
70 |
71 |
;
72 |
73 | export default App;
74 | ```
75 |
76 | ## Notes
77 |
78 | ### hovered prop
79 |
80 | The hovered prop is set to `true` on `MyHoverableComponent` when the mouse is hovering over it. Otherwise it's set to false.
81 |
82 | ### Choosing hoverId's
83 |
84 | As long as id's are different, they'll be independently hoverable. The above example sets the strings manually, but you could also use a `uuid()` too.
85 |
86 | This also means that ids with the same value will all get the hover state applied when any of them are hovered.
87 |
88 | ### MyHoverableComponent is cloned with `React.clone`
89 |
90 | This keeps the number of elements on the page minimal but adds a little overhead to clone the hoverable component.
91 |
92 | ### Manually Dispatching Actions For Hover Or Unhover
93 |
94 | Sometimes there's cases where manually dispatching an action might be necessary. A good example is a button that removes itself (clearing a todo list item for example). The actions are exposed for this purpose so they can be dispatched:
95 |
96 | ```js
97 | import {
98 | unhover,
99 | } from '@bufferapp/redux-hover';
100 |
101 | export const REMOVE_TODO = 'REMOVE_TODO';
102 |
103 | export const removeTodo = todoListId => dispatch =>
104 | Promise.all([
105 | dispatch({
106 | type: REMOVE_TODO,
107 | todoListId,
108 | }),
109 | dispatch(unhover(`todo-list-item/remove-${todoListId}`)),
110 | ]);
111 | ```
112 |
--------------------------------------------------------------------------------
/__tests__/action.js:
--------------------------------------------------------------------------------
1 | import {
2 | HOVER,
3 | UNHOVER,
4 | hover,
5 | unhover,
6 | } from '../src/action';
7 |
8 | describe('action', () => {
9 | it('HOVER action type exists', () => {
10 | expect(HOVER)
11 | .toBe('HOVER');
12 | });
13 | it('UNHOVER action type exists', () => {
14 | expect(UNHOVER)
15 | .toBe('UNHOVER');
16 | });
17 | describe('hover', () => {
18 | it('should create expected action', () => {
19 | const hoverId = 'some id';
20 | expect(hover(hoverId))
21 | .toEqual({
22 | type: HOVER,
23 | hoverId,
24 | });
25 | });
26 | });
27 | describe('unhover', () => {
28 | it('should create expected action', () => {
29 | const hoverId = 'some id';
30 | expect(unhover(hoverId))
31 | .toEqual({
32 | type: UNHOVER,
33 | hoverId,
34 | });
35 | });
36 | });
37 | });
38 |
--------------------------------------------------------------------------------
/__tests__/connectHoverable.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Provider } from 'react-redux';
3 | import { mount } from 'enzyme';
4 | import connectHoverable from '../src/connectHoverable';
5 | import {
6 | HOVER,
7 | UNHOVER,
8 | } from '../src/action';
9 |
10 | const storeFake = state => ({
11 | default: () => {},
12 | subscribe: () => {},
13 | dispatch: jest.fn(),
14 | getState: () => ({ ...state }),
15 | });
16 |
17 | /* eslint-disable react/prop-types */
18 |
19 | const TestComponent = ({
20 | children,
21 | hover,
22 | onMouseEnter,
23 | onMouseLeave,
24 | }) =>
25 |
30 | {children}
31 |
;
32 |
33 | const HoverableComponent = connectHoverable(TestComponent);
34 |
35 | /* eslint-enable react/prop-types */
36 |
37 | describe('HoverableContainer', () => {
38 | it('should render a container component', () => {
39 | const text = 'Hi!';
40 | const store = storeFake({});
41 | const wrapper = mount(
42 |
43 | {text}
44 | ,
45 | );
46 |
47 | expect(wrapper.find(TestComponent).text())
48 | .toBe(text);
49 | });
50 |
51 | it('should render a hovered component', () => {
52 | const text = 'Hi!';
53 | const hoverId = 'someid';
54 | const store = storeFake({
55 | hover: {
56 | [hoverId]: true,
57 | },
58 | });
59 | const wrapper = mount(
60 |
61 | {text}
62 | ,
63 | );
64 |
65 | expect(wrapper.find(TestComponent).props().hovered)
66 | .toBe(true);
67 | });
68 |
69 | it('should dispatch onMouseEnter event', () => {
70 | const text = 'Hi!';
71 | const hoverId = 'someid';
72 | const store = storeFake({});
73 | const wrapper = mount(
74 |
75 | {text}
76 | ,
77 | );
78 | wrapper.find(HoverableComponent).simulate('mouseEnter');
79 | expect(store.dispatch)
80 | .toBeCalledWith({
81 | type: HOVER,
82 | hoverId,
83 | });
84 | });
85 |
86 | it('should dispatch onMouseLeave event', () => {
87 | const text = 'Hi!';
88 | const hoverId = 'someid';
89 | const store = storeFake({});
90 | const wrapper = mount(
91 |
92 | {text}
93 | ,
94 | );
95 | wrapper.find(HoverableComponent).simulate('mouseLeave');
96 | expect(store.dispatch)
97 | .toBeCalledWith({
98 | type: UNHOVER,
99 | hoverId,
100 | });
101 | });
102 |
103 | it('should not dispatch onMouseEnter event without hoverId', () => {
104 | const text = 'Hi!';
105 | const store = storeFake({});
106 | const wrapper = mount(
107 |
108 | {text}
109 | ,
110 | );
111 | wrapper.find(HoverableComponent).simulate('mouseEnter');
112 | expect(store.dispatch)
113 | .not.toBeCalled();
114 | });
115 |
116 | it('should not dispatch onMouseLeave event without hoverId', () => {
117 | const text = 'Hi!';
118 | const store = storeFake({});
119 | const wrapper = mount(
120 |
121 | {text}
122 | ,
123 | );
124 | wrapper.find(HoverableComponent).simulate('mouseLeave');
125 | expect(store.dispatch)
126 | .not.toBeCalled();
127 | });
128 | });
129 |
--------------------------------------------------------------------------------
/__tests__/index.js:
--------------------------------------------------------------------------------
1 | import {
2 | reducer,
3 | connectHoverable,
4 | hover,
5 | unhover,
6 | } from '../src/index';
7 |
8 | describe('index', () => {
9 | it('should export the reducer', () => {
10 | expect(reducer)
11 | .toBeDefined();
12 | });
13 |
14 | it('should export the HoverableContainer', () => {
15 | expect(connectHoverable)
16 | .toBeDefined();
17 | });
18 | it('should export hover action', () => {
19 | expect(hover)
20 | .toBeDefined();
21 | });
22 | it('should export unhover action', () => {
23 | expect(unhover)
24 | .toBeDefined();
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/__tests__/reducer.js:
--------------------------------------------------------------------------------
1 | import reducer from '../src/reducer';
2 | import {
3 | HOVER,
4 | UNHOVER,
5 | } from '../src/action';
6 |
7 | describe('reducer', () => {
8 | it('should return initial state', () => {
9 | expect(reducer(undefined, {}))
10 | .toEqual({});
11 | });
12 | it('should handle HOVER', () => {
13 | const hoverId = 'some hoverId';
14 | expect(reducer(undefined, {
15 | type: HOVER,
16 | hoverId,
17 | }))
18 | .toEqual({
19 | [hoverId]: true,
20 | });
21 | });
22 | it('should handle hover with existing hover states', () => {
23 | const hoverId = 'some hoverId';
24 | const otherId = 'other hoverId';
25 | const initialState = { [otherId]: true };
26 | expect(reducer(initialState, {
27 | type: HOVER,
28 | hoverId,
29 | }))
30 | .toEqual({
31 | ...initialState,
32 | [hoverId]: true,
33 | });
34 | });
35 | it('should handle UNHOVER', () => {
36 | const hoverId = 'some hoverId';
37 | const initialState = { [hoverId]: true };
38 | expect(reducer(initialState, {
39 | type: UNHOVER,
40 | hoverId,
41 | }))
42 | .toEqual({});
43 | });
44 | it('should handle missing UNHOVER hoverId', () => {
45 | const hoverId = 'some hoverId';
46 | const initialState = {};
47 | expect(reducer(initialState, {
48 | type: UNHOVER,
49 | hoverId,
50 | }))
51 | .toEqual({});
52 | });
53 | });
54 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@bufferapp/redux-hover",
3 | "version": "0.0.6",
4 | "description": "Keep React component hover state in redux",
5 | "main": "./lib/index.js",
6 | "scripts": {
7 | "build": "babel src --out-dir lib",
8 | "lint": "eslint .",
9 | "test": "npm run lint && jest --coverage",
10 | "test-watch": "jest --watch",
11 | "prepublish": "npm run build"
12 | },
13 | "jest": {
14 | "coverageThreshold": {
15 | "global": {
16 | "branches": 100,
17 | "functions": 100,
18 | "lines": 100,
19 | "statements": 100
20 | }
21 | }
22 | },
23 | "repository": {
24 | "type": "git",
25 | "url": "git+https://github.com/bufferapp/buffer-redux-hover.git"
26 | },
27 | "keywords": [
28 | "react",
29 | "redux",
30 | "hover"
31 | ],
32 | "author": "Harrison Harnisch (http://hharnisc.github.io)",
33 | "license": "MIT",
34 | "bugs": {
35 | "url": "https://github.com/bufferapp/buffer-redux-hover/issues"
36 | },
37 | "homepage": "https://github.com/bufferapp/buffer-redux-hover#readme",
38 | "peerDependencies": {
39 | "react-redux": ">= 4.4.5"
40 | },
41 | "devDependencies": {
42 | "babel-cli": "^6.23.0",
43 | "babel-eslint": "^7.1.1",
44 | "babel-jest": "^18.0.0",
45 | "babel-preset-es2015": "^6.22.0",
46 | "babel-preset-react": "^6.23.0",
47 | "babel-preset-stage-0": "^6.22.0",
48 | "enzyme": "^2.7.1",
49 | "eslint": "^3.15.0",
50 | "eslint-config-airbnb": "^14.1.0",
51 | "eslint-plugin-import": "^2.2.0",
52 | "eslint-plugin-jsx-a11y": "^4.0.0",
53 | "eslint-plugin-react": "^6.9.0",
54 | "jest": "^18.1.0",
55 | "react": "^15.4.2",
56 | "react-addons-test-utils": "^15.4.2",
57 | "react-dom": "^15.4.2",
58 | "react-redux": "^5.0.2",
59 | "react-test-renderer": "^15.4.2",
60 | "redux": "^3.6.0"
61 | },
62 | "files": [
63 | "lib/",
64 | "src/",
65 | "README.md",
66 | "LICENSE"
67 | ]
68 | }
69 |
--------------------------------------------------------------------------------
/src/action.js:
--------------------------------------------------------------------------------
1 | export const HOVER = 'HOVER';
2 | export const UNHOVER = 'UNHOVER';
3 |
4 | export const hover = hoverId => ({
5 | type: HOVER,
6 | hoverId,
7 | });
8 |
9 | export const unhover = hoverId => ({
10 | type: UNHOVER,
11 | hoverId,
12 | });
13 |
--------------------------------------------------------------------------------
/src/connectHoverable.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import {
3 | hover,
4 | unhover,
5 | } from './action';
6 |
7 | const connectHoverable = (component) => {
8 | const mapStateToProps = (state, ownProps) => ({
9 | hovered: ownProps.hoverId && state.hover ? state.hover[ownProps.hoverId] : false,
10 | });
11 |
12 | const mapDispatchToProps = (dispatch, ownProps) => ({
13 | onMouseEnter: () => (ownProps.hoverId ? dispatch(hover(ownProps.hoverId)) : null),
14 | onMouseLeave: () => (ownProps.hoverId ? dispatch(unhover(ownProps.hoverId)) : null),
15 | });
16 |
17 | return connect(
18 | mapStateToProps,
19 | mapDispatchToProps,
20 | )(component);
21 | };
22 |
23 | export default connectHoverable;
24 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | export reducer from './reducer';
2 | export connectHoverable from './connectHoverable';
3 | export {
4 | hover,
5 | unhover,
6 | } from './action';
7 |
--------------------------------------------------------------------------------
/src/reducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | HOVER,
3 | UNHOVER,
4 | } from './action';
5 |
6 | const reducer = (state = {}, action) => {
7 | switch (action.type) {
8 | case HOVER:
9 | return { ...state, [action.hoverId]: true };
10 | case UNHOVER: {
11 | const newState = { ...state };
12 | delete newState[action.hoverId];
13 | return newState;
14 | }
15 | default:
16 | return state;
17 | }
18 | };
19 |
20 | export default reducer;
21 |
--------------------------------------------------------------------------------