Skip to content

Commit 67c6de4

Browse files
committed
First release
Signed-off-by: klimashkin <[email protected]>
1 parent 8755825 commit 67c6de4

13 files changed

+1159
-8
lines changed

.editorconfig

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
root = true
2+
3+
[*]
4+
charset = utf-8
5+
6+
indent_style = space
7+
indent_size = 2
8+
9+
end_of_line = lf
10+
insert_final_newline = false
11+
trim_trailing_whitespace = true

.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dist/**/*.js

.eslintrc.js

Lines changed: 608 additions & 0 deletions
Large diffs are not rendered by default.

.gitignore

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ Thumbs.db
1111
node_modules/
1212
jspm_packages/
1313

14+
# dist folder, users can get build files on unpkg
15+
dist/
16+
1417
# Optional npm cache directory
1518
.npm
1619

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

26-
# Coverage directory used by tools like istanbul
27-
coverage
28-
29-
# nyc test coverage
30-
.nyc_output
31-
3229
# Optional eslint cache
3330
.eslintcache
3431

.npmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
package-lock=false

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
## Change Log
2+
3+
### 1.0.0 (2018-03-15)
4+
- Initial release (@klimashkin)

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
# react-responsive-container
2-
Leverage ResizeObserver to add breakpoints to your component
1+
# react-size-watcher
2+
Leverage ResizeObserver by adding breakpoints to your component

package.json

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
{
2+
"name": "react-size-watcher",
3+
"version": "1.0.0",
4+
"author": "Pavel Klimashkin",
5+
"description": "Leverage ResizeObserver to add breakpoints to your component and track its size",
6+
"homepage": "https://github.com/klimashkin/react-size-watcher",
7+
"repository": {
8+
"type": "git",
9+
"url": "git+https://github.com/klimashkin/react-size-watcher.git"
10+
},
11+
"license": "MIT",
12+
"bugs": {
13+
"url": "https://github.com/klimashkin/react-size-watcher/issues"
14+
},
15+
"keywords": [
16+
"react",
17+
"responsive",
18+
"resize",
19+
"resizeobserver",
20+
"ResizeObserver",
21+
"react-size-watcher",
22+
"react-size-observer",
23+
"media query"
24+
],
25+
"main": "./dist/react-size-watcher.js",
26+
"es2015": "./dist/es2015/react-size-watcher.js",
27+
"es2017": "./dist/es2017/react-size-watcher.js",
28+
"files": [
29+
"dist",
30+
"src"
31+
],
32+
"peerDependencies": {
33+
"react": "^16.2.0"
34+
},
35+
"dependencies": {
36+
"prop-types": "^15.6.1"
37+
},
38+
"devDependencies": {
39+
"babel-cli": "^6.26.0",
40+
"babel-core": "^6.26.0",
41+
"babel-eslint": "^8.2.2",
42+
"babel-loader": "^7.1.4",
43+
"babel-plugin-syntax-async-functions": "6.13.0",
44+
"babel-plugin-syntax-trailing-function-commas": "6.22.0",
45+
"babel-plugin-transform-async-to-generator": "6.24.1",
46+
"babel-plugin-transform-class-properties": "^6.24.1",
47+
"babel-plugin-transform-object-rest-spread": "^6.26.0",
48+
"babel-plugin-transform-react-remove-prop-types": "0.4.13",
49+
"babel-preset-env": "^1.6.1",
50+
"babel-preset-react": "^6.24.1",
51+
"cross-env": "^5.1.4",
52+
"eslint": "^4.18.2",
53+
"eslint-plugin-babel": "^4.1.2",
54+
"eslint-plugin-import": "^2.9.0",
55+
"eslint-plugin-react": "^7.7.0",
56+
"github-changes": "^1.1.2",
57+
"null-loader": "0.1.1",
58+
"react": "^16.2.0",
59+
"react-dom": "^16.2.0",
60+
"rimraf": "2.6.2",
61+
"uglifyjs-webpack-plugin": "1.2.3",
62+
"webpack": "^4.1.1",
63+
"webpack-cli": "2.0.12"
64+
},
65+
"scripts": {
66+
"preversion": "npm run lint && npm run clean && npm run build",
67+
"postversion": "npm run changelog",
68+
"build:umd": "cross-env BUILD_MODE=umd webpack",
69+
"build:umd-min": "cross-env BUILD_MODE=umd-min webpack",
70+
"build:es6": "cross-env BUILD_MODE=es2015 webpack",
71+
"build:es6-min": "cross-env BUILD_MODE=es2015-min webpack",
72+
"build:es8": "cross-env BUILD_MODE=es2017 webpack",
73+
"build:es8-min": "cross-env BUILD_MODE=es2017-min webpack",
74+
"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",
75+
"clean": "rimraf dist",
76+
"lint": "eslint ./",
77+
"changelog": "github-changes -o klimashkin -r react-size-watcher -b master -f ./CHANGELOG.md --order-semver --use-commit-body"
78+
},
79+
"engines": {
80+
"node": ">=6.0.0"
81+
}
82+
}

src/SizeWatcher.js

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
import PropTypes from 'prop-types';
2+
import {Component, createElement} from 'react';
3+
import {contextTypes} from './SizeWatcherTypes';
4+
5+
const defaultBreakpoint = {props: {}, minWidth: 0, maxWidth: Infinity, minHeight: 0, maxHeight: Infinity};
6+
7+
export default class SizeWatcher extends Component {
8+
static contextTypes = contextTypes;
9+
10+
static propTypes = {
11+
// Array of breakpoints
12+
breakpoints: PropTypes.arrayOf(PropTypes.shape({
13+
// Dimensions if breakpoint
14+
minWidth: PropTypes.number,
15+
maxWidth: PropTypes.number,
16+
minHeight: PropTypes.number,
17+
maxHeight: PropTypes.number,
18+
// Props that are mixed into container props
19+
props: PropTypes.object,
20+
// Property that contains any custom data useful in child render function
21+
data: PropTypes.any,
22+
})).isRequired,
23+
24+
// Breakpoint match strategy
25+
// 'order' (default) - match by breakpoints order, like in css @media-queries, when the last match wins
26+
// 'breakpointArea' - match by breakpoints area, when the smallest area is considered to be more specific
27+
// 'intersectionArea' - match by area of breakpoint/size intersection,
28+
// when the biggest intersection area is considered to be more specific
29+
matchBy: PropTypes.oneOf(['order', 'breakpointArea', 'intersectionArea']),
30+
31+
// By default only container will be rendered on first render() call (because usually it's a block element)
32+
// to compute its width and decide what breakpoint pass down to render function.
33+
// But if container is a part of flex, or grow according to its content, or breakpoint depend on height,
34+
// content should be rendered with visibility:hidden before breakpoint computation.
35+
renderContentOnInit: PropTypes.bool, // By default - false
36+
// If renderContentOnInit is true, pass this flag to show content on first render before computation
37+
showOnInit: PropTypes.bool, // By default - false
38+
39+
// Container can mimic any html element ('div', 'h2' etc) or custom component (constructors like Link, Button etc)
40+
type: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
41+
42+
// Render function, that is called on breakpoint change
43+
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
44+
45+
// Optional callback on each size change
46+
onSizeChange: PropTypes.func,
47+
};
48+
49+
static defaultProps = {
50+
renderContentOnInit: false,
51+
showOnInit: false,
52+
matchBy: 'order',
53+
type: 'div',
54+
};
55+
56+
constructor(props, context) {
57+
super(props, context);
58+
59+
this.processBreakpoints(props.breakpoints);
60+
this.state = {
61+
breakpoint: props.renderContentOnInit ? this.findBreakpoint() : null,
62+
};
63+
64+
this.saveRef = this.saveRef.bind(this);
65+
}
66+
67+
componentWillReceiveProps(nextProps) {
68+
const {props} = this;
69+
70+
if (nextProps.breakpoints !== props.breakpoints || nextProps.matchBy !== props.matchBy) {
71+
this.processBreakpoints(nextProps.breakpoints);
72+
73+
const breakpoint = this.findBreakpoint(this.size);
74+
75+
if (breakpoint !== this.state.breakpoint) {
76+
this.setState({breakpoint});
77+
}
78+
}
79+
}
80+
81+
componentWillUnmount() {
82+
this.checkOut();
83+
}
84+
85+
saveRef(dom) {
86+
if (dom) {
87+
// If type props contains custom react components, ref returns instance of that component
88+
// Try taking dom by calling method getWrappedInstance on it
89+
while (typeof dom.getWrappedInstance === 'function') {
90+
dom = dom.getWrappedInstance();
91+
}
92+
93+
this.checkIn(dom);
94+
} else {
95+
this.checkOut();
96+
}
97+
}
98+
99+
checkIn(dom) {
100+
this.dom = dom;
101+
this.context.sizeWatcher.checkIn(this, dom);
102+
}
103+
104+
checkOut() {
105+
if (this.dom) {
106+
this.context.sizeWatcher.checkOut(this.dom);
107+
this.dom = null;
108+
}
109+
}
110+
111+
updateSize(size) {
112+
if (this.size === undefined || size.width !== this.size.width || size.height !== this.size.height) {
113+
const breakpointNeedCheck =
114+
this.size === undefined ||
115+
this.sensitiveToWidth && size.width !== this.size.width ||
116+
this.sensitiveToHeight && size.height !== this.size.height;
117+
118+
const currentBreakpoint = this.state.breakpoint;
119+
const newBreakpoint = breakpointNeedCheck ? this.findBreakpoint(size) : currentBreakpoint;
120+
121+
this.size = size;
122+
123+
if (typeof this.props.onSizeChange === 'function') {
124+
this.props.onSizeChange(size, currentBreakpoint, newBreakpoint);
125+
}
126+
127+
if (newBreakpoint !== currentBreakpoint) {
128+
this.setState({breakpoint: newBreakpoint});
129+
}
130+
}
131+
}
132+
133+
processBreakpoints(breakpoints) {
134+
this.sensitiveToWidth = false;
135+
this.sensitiveToHeight = false;
136+
this.breakpoints = breakpoints;
137+
138+
for (const {minWidth, maxWidth, minHeight, maxHeight} of breakpoints) {
139+
// Validate maxWidth/minWidth in development
140+
const maxWidthType = typeof maxWidth;
141+
const minWidthType = typeof minWidth;
142+
const maxHeightType = typeof maxHeight;
143+
const minHeightType = typeof minHeight;
144+
145+
if (maxWidthType !== 'undefined' && (maxWidthType !== 'number' || maxWidth < 0) ||
146+
minWidthType !== 'undefined' && (minWidthType !== 'number' || minWidth < 0 || maxWidth && minWidth > maxWidth)) {
147+
throw Error(`Wrong min ${JSON.stringify(minWidth)} or max ${JSON.stringify(maxWidth)} width in breakpoint`);
148+
}
149+
150+
if (maxHeightType !== 'undefined' && (maxHeightType !== 'number' || maxHeight < 0) ||
151+
minHeightType !== 'undefined' && (minHeightType !== 'number' || minHeight < 0 || maxHeight && minHeight > maxHeight)) {
152+
throw Error(`Wrong min ${JSON.stringify(minHeight)} or max ${JSON.stringify(maxHeight)} height in breakpoint`);
153+
}
154+
155+
if (!this.sensitiveToWidth && (minWidth || maxWidth)) {
156+
this.sensitiveToWidth = true;
157+
}
158+
159+
if (!this.sensitiveToHeight && (minHeight || maxHeight)) {
160+
this.sensitiveToHeight = true;
161+
}
162+
}
163+
164+
if (this.props.matchBy === 'order') {
165+
this.findBreakpoint = this.findBreakpointByOrder;
166+
} else if (this.props.matchBy === 'breakpointArea') {
167+
this.findBreakpoint = this.findBreakpointByArea;
168+
} else if (this.props.matchBy === 'intersectionArea') {
169+
this.findBreakpoint = this.findBreakpointByIntersection;
170+
}
171+
}
172+
173+
// Find the last breakpoint that matches given size
174+
findBreakpointByOrder({width = Infinity, height = Infinity} = {}) {
175+
for (let i = this.breakpoints.length; i--;) {
176+
const breakpoint = this.breakpoints[i];
177+
const {minWidth = 0, maxWidth = Infinity, minHeight = 0, maxHeight = Infinity} = breakpoint;
178+
179+
if (width >= minWidth && width <= maxWidth && height >= minHeight && height <= maxHeight) {
180+
return breakpoint;
181+
}
182+
}
183+
184+
return defaultBreakpoint;
185+
}
186+
187+
// Find breakpoint with the smallest area
188+
findBreakpointByArea({width = 1e6, height = 1e6} = {}) {
189+
return this.breakpoints.reduce((result, breakpoint) => {
190+
const {minWidth = 0, maxWidth = 1e6, minHeight = 0, maxHeight = 1e6} = breakpoint;
191+
192+
if (width >= minWidth && width <= maxWidth && height >= minHeight && height <= maxHeight) {
193+
const area = (maxWidth - minWidth) * (maxHeight - minHeight);
194+
195+
if (area <= result.area) {
196+
result.area = area;
197+
result.breakpoint = breakpoint;
198+
}
199+
}
200+
201+
return result;
202+
}, {area: Infinity, breakpoint: defaultBreakpoint}).breakpoint;
203+
}
204+
205+
// Find breakpoint with the biggest breakpoint/size intersection area
206+
findBreakpointByIntersection({width = 1e6, height = 1e6} = {}) {
207+
return this.breakpoints.reduce((result, breakpoint) => {
208+
const {minWidth = 0, maxWidth = 1e6, minHeight = 0, maxHeight = 1e6} = breakpoint;
209+
const area = Math.max(0, Math.min(maxWidth, width) - minWidth) * Math.max(0, Math.min(maxHeight, height) - minHeight);
210+
211+
if (area >= result.area) {
212+
result.area = area;
213+
result.breakpoint = breakpoint;
214+
}
215+
216+
return result;
217+
}, {area: 0, breakpoint: defaultBreakpoint}).breakpoint;
218+
}
219+
220+
render() {
221+
const {
222+
props: {
223+
type, breakpoints, matchBy, renderContentOnInit, showOnInit, children, onSizeChange,
224+
...elementProps
225+
},
226+
state: {breakpoint},
227+
} = this;
228+
229+
let renderedChildren;
230+
231+
if (breakpoint !== null) {
232+
renderedChildren = typeof children === 'function' ? children(breakpoint, this.size) : children;
233+
234+
Object.assign(elementProps, breakpoint.props);
235+
236+
if (this.size === undefined && !showOnInit) {
237+
elementProps.style = {...elementProps.style, visibility: 'hidden'};
238+
}
239+
}
240+
241+
elementProps.ref = this.saveRef;
242+
243+
return createElement(type, elementProps, renderedChildren);
244+
}
245+
}

0 commit comments

Comments
 (0)