Skip to content

Commit

Permalink
First release
Browse files Browse the repository at this point in the history
Signed-off-by: klimashkin <[email protected]>
  • Loading branch information
klimashkin committed Mar 16, 2018
1 parent 8755825 commit 67c6de4
Show file tree
Hide file tree
Showing 13 changed files with 1,159 additions and 8 deletions.
11 changes: 11 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
root = true

[*]
charset = utf-8

indent_style = space
indent_size = 2

end_of_line = lf
insert_final_newline = false
trim_trailing_whitespace = true
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist/**/*.js
608 changes: 608 additions & 0 deletions .eslintrc.js

Large diffs are not rendered by default.

9 changes: 3 additions & 6 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ Thumbs.db
node_modules/
jspm_packages/

# dist folder, users can get build files on unpkg
dist/

# Optional npm cache directory
.npm

Expand All @@ -23,12 +26,6 @@ jspm_packages/
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Optional eslint cache
.eslintcache

Expand Down
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package-lock=false
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
## Change Log

### 1.0.0 (2018-03-15)
- Initial release (@klimashkin)
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# react-responsive-container
Leverage ResizeObserver to add breakpoints to your component
# react-size-watcher
Leverage ResizeObserver by adding breakpoints to your component
82 changes: 82 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
{
"name": "react-size-watcher",
"version": "1.0.0",
"author": "Pavel Klimashkin",
"description": "Leverage ResizeObserver to add breakpoints to your component and track its size",
"homepage": "https://github.com/klimashkin/react-size-watcher",
"repository": {
"type": "git",
"url": "git+https://github.com/klimashkin/react-size-watcher.git"
},
"license": "MIT",
"bugs": {
"url": "https://github.com/klimashkin/react-size-watcher/issues"
},
"keywords": [
"react",
"responsive",
"resize",
"resizeobserver",
"ResizeObserver",
"react-size-watcher",
"react-size-observer",
"media query"
],
"main": "./dist/react-size-watcher.js",
"es2015": "./dist/es2015/react-size-watcher.js",
"es2017": "./dist/es2017/react-size-watcher.js",
"files": [
"dist",
"src"
],
"peerDependencies": {
"react": "^16.2.0"
},
"dependencies": {
"prop-types": "^15.6.1"
},
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-core": "^6.26.0",
"babel-eslint": "^8.2.2",
"babel-loader": "^7.1.4",
"babel-plugin-syntax-async-functions": "6.13.0",
"babel-plugin-syntax-trailing-function-commas": "6.22.0",
"babel-plugin-transform-async-to-generator": "6.24.1",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-plugin-transform-react-remove-prop-types": "0.4.13",
"babel-preset-env": "^1.6.1",
"babel-preset-react": "^6.24.1",
"cross-env": "^5.1.4",
"eslint": "^4.18.2",
"eslint-plugin-babel": "^4.1.2",
"eslint-plugin-import": "^2.9.0",
"eslint-plugin-react": "^7.7.0",
"github-changes": "^1.1.2",
"null-loader": "0.1.1",
"react": "^16.2.0",
"react-dom": "^16.2.0",
"rimraf": "2.6.2",
"uglifyjs-webpack-plugin": "1.2.3",
"webpack": "^4.1.1",
"webpack-cli": "2.0.12"
},
"scripts": {
"preversion": "npm run lint && npm run clean && npm run build",
"postversion": "npm run changelog",
"build:umd": "cross-env BUILD_MODE=umd webpack",
"build:umd-min": "cross-env BUILD_MODE=umd-min webpack",
"build:es6": "cross-env BUILD_MODE=es2015 webpack",
"build:es6-min": "cross-env BUILD_MODE=es2015-min webpack",
"build:es8": "cross-env BUILD_MODE=es2017 webpack",
"build:es8-min": "cross-env BUILD_MODE=es2017-min webpack",
"build": "npm run build:umd && npm run build:umd-min && npm run build:es6 && npm run build:es6-min && npm run build:es8 && npm run build:es8-min",
"clean": "rimraf dist",
"lint": "eslint ./",
"changelog": "github-changes -o klimashkin -r react-size-watcher -b master -f ./CHANGELOG.md --order-semver --use-commit-body"
},
"engines": {
"node": ">=6.0.0"
}
}
245 changes: 245 additions & 0 deletions src/SizeWatcher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
import PropTypes from 'prop-types';
import {Component, createElement} from 'react';
import {contextTypes} from './SizeWatcherTypes';

const defaultBreakpoint = {props: {}, minWidth: 0, maxWidth: Infinity, minHeight: 0, maxHeight: Infinity};

export default class SizeWatcher extends Component {
static contextTypes = contextTypes;

static propTypes = {
// Array of breakpoints
breakpoints: PropTypes.arrayOf(PropTypes.shape({
// Dimensions if breakpoint
minWidth: PropTypes.number,
maxWidth: PropTypes.number,
minHeight: PropTypes.number,
maxHeight: PropTypes.number,
// Props that are mixed into container props
props: PropTypes.object,
// Property that contains any custom data useful in child render function
data: PropTypes.any,
})).isRequired,

// Breakpoint match strategy
// 'order' (default) - match by breakpoints order, like in css @media-queries, when the last match wins
// 'breakpointArea' - match by breakpoints area, when the smallest area is considered to be more specific
// 'intersectionArea' - match by area of breakpoint/size intersection,
// when the biggest intersection area is considered to be more specific
matchBy: PropTypes.oneOf(['order', 'breakpointArea', 'intersectionArea']),

// By default only container will be rendered on first render() call (because usually it's a block element)
// to compute its width and decide what breakpoint pass down to render function.
// But if container is a part of flex, or grow according to its content, or breakpoint depend on height,
// content should be rendered with visibility:hidden before breakpoint computation.
renderContentOnInit: PropTypes.bool, // By default - false
// If renderContentOnInit is true, pass this flag to show content on first render before computation
showOnInit: PropTypes.bool, // By default - false

// Container can mimic any html element ('div', 'h2' etc) or custom component (constructors like Link, Button etc)
type: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),

// Render function, that is called on breakpoint change
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,

// Optional callback on each size change
onSizeChange: PropTypes.func,
};

static defaultProps = {
renderContentOnInit: false,
showOnInit: false,
matchBy: 'order',
type: 'div',
};

constructor(props, context) {
super(props, context);

this.processBreakpoints(props.breakpoints);
this.state = {
breakpoint: props.renderContentOnInit ? this.findBreakpoint() : null,
};

this.saveRef = this.saveRef.bind(this);
}

componentWillReceiveProps(nextProps) {
const {props} = this;

if (nextProps.breakpoints !== props.breakpoints || nextProps.matchBy !== props.matchBy) {
this.processBreakpoints(nextProps.breakpoints);

const breakpoint = this.findBreakpoint(this.size);

if (breakpoint !== this.state.breakpoint) {
this.setState({breakpoint});
}
}
}

componentWillUnmount() {
this.checkOut();
}

saveRef(dom) {
if (dom) {
// If type props contains custom react components, ref returns instance of that component
// Try taking dom by calling method getWrappedInstance on it
while (typeof dom.getWrappedInstance === 'function') {
dom = dom.getWrappedInstance();
}

this.checkIn(dom);
} else {
this.checkOut();
}
}

checkIn(dom) {
this.dom = dom;
this.context.sizeWatcher.checkIn(this, dom);
}

checkOut() {
if (this.dom) {
this.context.sizeWatcher.checkOut(this.dom);
this.dom = null;
}
}

updateSize(size) {
if (this.size === undefined || size.width !== this.size.width || size.height !== this.size.height) {
const breakpointNeedCheck =
this.size === undefined ||
this.sensitiveToWidth && size.width !== this.size.width ||
this.sensitiveToHeight && size.height !== this.size.height;

const currentBreakpoint = this.state.breakpoint;
const newBreakpoint = breakpointNeedCheck ? this.findBreakpoint(size) : currentBreakpoint;

this.size = size;

if (typeof this.props.onSizeChange === 'function') {
this.props.onSizeChange(size, currentBreakpoint, newBreakpoint);
}

if (newBreakpoint !== currentBreakpoint) {
this.setState({breakpoint: newBreakpoint});
}
}
}

processBreakpoints(breakpoints) {
this.sensitiveToWidth = false;
this.sensitiveToHeight = false;
this.breakpoints = breakpoints;

for (const {minWidth, maxWidth, minHeight, maxHeight} of breakpoints) {
// Validate maxWidth/minWidth in development
const maxWidthType = typeof maxWidth;
const minWidthType = typeof minWidth;
const maxHeightType = typeof maxHeight;
const minHeightType = typeof minHeight;

if (maxWidthType !== 'undefined' && (maxWidthType !== 'number' || maxWidth < 0) ||
minWidthType !== 'undefined' && (minWidthType !== 'number' || minWidth < 0 || maxWidth && minWidth > maxWidth)) {
throw Error(`Wrong min ${JSON.stringify(minWidth)} or max ${JSON.stringify(maxWidth)} width in breakpoint`);
}

if (maxHeightType !== 'undefined' && (maxHeightType !== 'number' || maxHeight < 0) ||
minHeightType !== 'undefined' && (minHeightType !== 'number' || minHeight < 0 || maxHeight && minHeight > maxHeight)) {
throw Error(`Wrong min ${JSON.stringify(minHeight)} or max ${JSON.stringify(maxHeight)} height in breakpoint`);
}

if (!this.sensitiveToWidth && (minWidth || maxWidth)) {
this.sensitiveToWidth = true;
}

if (!this.sensitiveToHeight && (minHeight || maxHeight)) {
this.sensitiveToHeight = true;
}
}

if (this.props.matchBy === 'order') {
this.findBreakpoint = this.findBreakpointByOrder;
} else if (this.props.matchBy === 'breakpointArea') {
this.findBreakpoint = this.findBreakpointByArea;
} else if (this.props.matchBy === 'intersectionArea') {
this.findBreakpoint = this.findBreakpointByIntersection;
}
}

// Find the last breakpoint that matches given size
findBreakpointByOrder({width = Infinity, height = Infinity} = {}) {
for (let i = this.breakpoints.length; i--;) {
const breakpoint = this.breakpoints[i];
const {minWidth = 0, maxWidth = Infinity, minHeight = 0, maxHeight = Infinity} = breakpoint;

if (width >= minWidth && width <= maxWidth && height >= minHeight && height <= maxHeight) {
return breakpoint;
}
}

return defaultBreakpoint;
}

// Find breakpoint with the smallest area
findBreakpointByArea({width = 1e6, height = 1e6} = {}) {
return this.breakpoints.reduce((result, breakpoint) => {
const {minWidth = 0, maxWidth = 1e6, minHeight = 0, maxHeight = 1e6} = breakpoint;

if (width >= minWidth && width <= maxWidth && height >= minHeight && height <= maxHeight) {
const area = (maxWidth - minWidth) * (maxHeight - minHeight);

if (area <= result.area) {
result.area = area;
result.breakpoint = breakpoint;
}
}

return result;
}, {area: Infinity, breakpoint: defaultBreakpoint}).breakpoint;
}

// Find breakpoint with the biggest breakpoint/size intersection area
findBreakpointByIntersection({width = 1e6, height = 1e6} = {}) {
return this.breakpoints.reduce((result, breakpoint) => {
const {minWidth = 0, maxWidth = 1e6, minHeight = 0, maxHeight = 1e6} = breakpoint;
const area = Math.max(0, Math.min(maxWidth, width) - minWidth) * Math.max(0, Math.min(maxHeight, height) - minHeight);

if (area >= result.area) {
result.area = area;
result.breakpoint = breakpoint;
}

return result;
}, {area: 0, breakpoint: defaultBreakpoint}).breakpoint;
}

render() {
const {
props: {
type, breakpoints, matchBy, renderContentOnInit, showOnInit, children, onSizeChange,
...elementProps
},
state: {breakpoint},
} = this;

let renderedChildren;

if (breakpoint !== null) {
renderedChildren = typeof children === 'function' ? children(breakpoint, this.size) : children;

Object.assign(elementProps, breakpoint.props);

if (this.size === undefined && !showOnInit) {
elementProps.style = {...elementProps.style, visibility: 'hidden'};
}
}

elementProps.ref = this.saveRef;

return createElement(type, elementProps, renderedChildren);
}
}
Loading

0 comments on commit 67c6de4

Please sign in to comment.