Skip to content

Commit

Permalink
Merge pull request #91 from williaster/chris--withBoundingRects
Browse files Browse the repository at this point in the history
[vx-bounds] add package with 'withBoundingRects()' HOC and tests
  • Loading branch information
hshoff authored Jul 7, 2017
2 parents ceabf02 + a83d1f7 commit 2cf9ade
Show file tree
Hide file tree
Showing 7 changed files with 264 additions and 0 deletions.
19 changes: 19 additions & 0 deletions packages/vx-bounds/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"presets": ["es2015", "react", "stage-0"],
"plugins": [],
"env": {
"development": {
"plugins": [
["react-transform", {
"transforms": [{
"transform": "react-transform-hmr",
"imports": ["react"],
"locals": ["module"]
}]
}],
"transform-runtime",
"transform-decorators-legacy"
]
}
}
}
1 change: 1 addition & 0 deletions packages/vx-bounds/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include node_modules/react-fatigue-dev/Makefile
56 changes: 56 additions & 0 deletions packages/vx-bounds/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# @vx/bounds

```
npm install --save @vx/bounds
```

### `withBoundingRects` HOC
It's often useful to determine whether elements (e.g., tooltips) overflow the bounds of their parent container and adjust positioning accordingly. The `withBoundingRects` higher-order component is meant to simplify this computation by passing in a component's bounding rect as well as its _parent's_ bounding rect.


### Example usage
Example usage with a `<Tooltip />` component

```javascript
import React from 'react';
import PropTypes from 'prop-types';
import { withBoundingRects, withBoundingRectsProps } from '@vx/bounds';

const propTypes = {
...withBoundingRectsProps,
left: PropTypes.number.isRequired,
top: PropTypes.number.isRequired,
children: PropTypes.node,
};

const defaultProps = {
children: null,
};

function Tooltip({
left: initialLeft,
top: initialTop,
rect,
parentRect,
children,
}) {
let left = initialLeft;
let top = initialTop;

if (rect && parentRect) {
left = rect.right > parentRect.right ? (left - rect.width) : left;
top = rect.bottom > parentRect.bottom ? (top - rect.height) : top;
}

return (
<div style={{ top, left, ...myTheme }}>
{children}
</div>
);
}

Tooltip.propTypes = propTypes;
Tooltip.defaultProps = defaultProps;

export default withBoundingRects(Tooltip);
```
50 changes: 50 additions & 0 deletions packages/vx-bounds/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"name": "@vx/bounds",
"version": "0.0.0",
"description": "Utilities to make your life with bounding boxes better",
"main": "build/index.js",
"scripts": {
"build": "make build SRC=./src",
"prepublish": "make build SRC=./src",
"test": "jest"
},
"files": [
"build"
],
"repository": {
"type": "git",
"url": "git+https://github.com/hshoff/vx.git"
},
"keywords": [
"vx",
"react",
"visualizations",
"charts"
],
"author": "Chris Williams @williaster",
"license": "MIT",
"bugs": {
"url": "https://github.com/hshoff/vx/issues"
},
"homepage": "https://github.com/hshoff/vx#readme",
"publishConfig": {
"access": "public"
},
"devDependencies": {
"babel-jest": "^20.0.3",
"enzyme": "^2.8.2",
"jest": "^20.0.3",
"react": "^15.0.0 || 15.x",
"react-addons-test-utils": "^15.4.2",
"react-fatigue-dev": "github:tj/react-fatigue-dev",
"react-tools": "^0.10.0",
"regenerator-runtime": "^0.10.5"
},
"dependencies": {
"prop-types": "^15.5.10",
"react-dom": "^15.0.0 || 15.x"
},
"peerDependencies": {
"react": "^15.0.0 || 15.x"
}
}
79 changes: 79 additions & 0 deletions packages/vx-bounds/src/enhancers/withBoundingRects.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/* eslint react/no-did-mount-set-state: 0, react/no-find-dom-node: 0 */
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';

const emptyRect = {
top: 0,
right: 0,
bottom: 0,
left: 0,
width: 0,
height: 0,
};

const rectShape = PropTypes.shape({
top: PropTypes.number.isRequired,
right: PropTypes.number.isRequired,
bottom: PropTypes.number.isRequired,
left: PropTypes.number.isRequired,
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
});

export const withBoundingRectsProps = {
getRects: PropTypes.func,
rect: rectShape,
parentRect: rectShape,
};

export default function withBoundingRects(BaseComponent) {
class WrappedComponent extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
rect: undefined,
parentRect: undefined,
};
this.getRects = this.getRects.bind(this);
}

componentDidMount() {
this.node = ReactDOM.findDOMNode(this);
this.setState(this.getRects());
}

getRects() {
if (!this.node) return this.state;

const node = this.node;
const parentNode = this.node.parentNode;

const rect = node.getBoundingClientRect
? node.getBoundingClientRect()
: emptyRect;

const parentRect = parentNode && parentNode.getBoundingClientRect
? parentNode.getBoundingClientRect()
: emptyRect;

return { rect, parentRect };
}

render() {
return (
<BaseComponent
getRects={this.getRects}
{...this.state}
{...this.props}
/>
);
}
}

WrappedComponent.propTypes = BaseComponent.propTypes;
WrappedComponent.defaultProps = BaseComponent.defaultProps;
WrappedComponent.displayName = `withBoundingRects(${BaseComponent.displayName || ''})`;

return WrappedComponent;
}
1 change: 1 addition & 0 deletions packages/vx-bounds/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as withBoundingRects, withBoundingRectsProps } from './enhancers/withBoundingRects';
58 changes: 58 additions & 0 deletions packages/vx-bounds/test/withBoundingRects.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React from 'react';
import { mount } from 'enzyme';

import { withBoundingRects, withBoundingRectsProps } from '../src/';

const expectedRectShape = expect.objectContaining({
top: expect.any(Number),
right: expect.any(Number),
bottom: expect.any(Number),
left: expect.any(Number),
width: expect.any(Number),
height: expect.any(Number),
});

describe('withBoundingRects()', () => {
beforeAll(() => { // mock getBoundingClientRect
Element.prototype.getBoundingClientRect = jest.fn(() => ({
width: 100,
height: 100,
top: 0,
left: 0,
bottom: 0,
right: 0,
}));
});

function Component() {
return <div />;
}

test('it should be defined', () => {
expect(withBoundingRects).toBeDefined();
});

test('it should pass rect, parentRect, and getRect props to the wrapped component', () => {
const HOC = withBoundingRects(Component);
const wrapper = mount(<HOC />);
const RenderedComponent = wrapper.find(Component);

expect(Element.prototype.getBoundingClientRect).toHaveBeenCalled();
expect(RenderedComponent.prop('rect')).toEqual(expectedRectShape);
expect(RenderedComponent.prop('parentRect')).toEqual(expectedRectShape);
expect(typeof RenderedComponent.prop('getRects')).toBe('function');
});

test('it should pass additional props to the wrapped component', () => {
const HOC = withBoundingRects(Component);
const wrapper = mount(<HOC bananas="are yellow" />);
const RenderedComponent = wrapper.find(Component);
expect(RenderedComponent.prop('bananas')).toBe('are yellow');
});
});

describe('withBoundingRectsProps', () => {
test('it should be defined', () => {
expect(withBoundingRectsProps).toBeDefined();
});
});

0 comments on commit 2cf9ade

Please sign in to comment.