Skip to content

Commit ac11673

Browse files
authored
Merge pull request #18 from faceless-ui/feature/custom-breakpoints
customizable breakpoints
2 parents 2830715 + 37f3ad4 commit ac11673

File tree

12 files changed

+233
-190
lines changed

12 files changed

+233
-190
lines changed

demo/App.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,17 @@ import UseWindowInfo from './UseWindowInfo.demo';
99
// import LogProps from './LogProps';
1010

1111
const breakpoints = {
12-
xs: 350,
13-
s: 576,
14-
m: 850,
15-
l: 992,
16-
xl: 1200,
12+
'mobile-first-xs': '(min-width: 350px)',
13+
'mobile-first-s': '(min-width: 576px)',
14+
'mobile-first-m': '(min-width: 850px)',
15+
'mobile-first-l': '(min-width: 992px)',
16+
'mobile-first-xl': '(min-width: 1200px)',
17+
18+
'desktop-first-xs': '(max-width: 350px)',
19+
'desktop-first-s': '(max-width: 576px)',
20+
'desktop-first-m': '(max-width: 850px)',
21+
'desktop-first-l': '(max-width: 992px)',
22+
'desktop-first-xl': '(max-width: 1200px)',
1723
};
1824

1925
const AppDemo: React.FC = () => (

demo/LogProps.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react';
2-
import { IWindowInfoContext } from '../src/WindowInfoContext/types';
2+
import { IWindowInfoContext } from '../src/WindowInfoContext';
33

44
const filterObject = () => {
55
const seen = new WeakSet();

demo/Stylesheet.demo.tsx

Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
11
import React, { Fragment } from 'react';
2+
import { Breakpoints } from '../src/types';
23

34
type Props = {
4-
breakpoints: {
5-
xs: number,
6-
s: number,
7-
m: number,
8-
l: number,
9-
xl: number
10-
}
5+
breakpoints: Breakpoints
116
}
127

138
const StylesheetDemo: React.FC<Props> = (props) => {
@@ -22,27 +17,20 @@ const StylesheetDemo: React.FC<Props> = (props) => {
2217
<Fragment>
2318
<style
2419
dangerouslySetInnerHTML={{
25-
__html: hasBreakpoints && breakpointsKeys.map((key) => `@media(max-width: ${breakpoints[key]}px) { #${key} { color: green; } }`).join(' '),
20+
__html: hasBreakpoints && breakpointsKeys.map((key) => `@media${breakpoints[key]} { #${key} { color: green; } }`).join(' '),
2621
}}
2722
/>
2823
<code>
2924
<pre>
3025
@media:
31-
<div id="xs">
32-
xs
33-
</div>
34-
<div id="s">
35-
s
36-
</div>
37-
<div id="m">
38-
m
39-
</div>
40-
<div id="l">
41-
l
42-
</div>
43-
<div id="xl">
44-
xl
45-
</div>
26+
{hasBreakpoints && breakpointsKeys.map((breakpointKey) => (
27+
<div
28+
key={breakpointKey}
29+
id={breakpointKey}
30+
>
31+
{`${breakpointKey}: ${breakpoints[breakpointKey]}`}
32+
</div>
33+
))}
4634
</pre>
4735
</code>
4836
</Fragment>

demo/WithWindowInfo.demo.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { withWindowInfo } from '../src'; // swap '../src' for '../dist/build.bundle' to demo production build
2-
import { IWindowInfoContext } from '../src/WindowInfoContext/types';
2+
import { IWindowInfoContext } from '../src/WindowInfoContext';
33
import LogProps from './LogProps';
44

55
type Props = {

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@
3535
"@types/node": "^14.14.22",
3636
"@types/react": "^17.0.0",
3737
"@trbl/utils": "^1.1.1",
38-
"@typescript-eslint/eslint-plugin": "^4.11.1",
39-
"@typescript-eslint/parser": "^4.11.1",
38+
"@typescript-eslint/eslint-plugin": "^4.22.1",
39+
"@typescript-eslint/parser": "^4.22.1",
4040
"enzyme": "^3.11.0",
4141
"enzyme-adapter-react-16": "^1.15.2",
4242
"eslint": "^7.16.0",

src/WindowInfoContext/index.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
import { createContext } from 'react';
2-
import { IWindowInfoContext } from './types';
2+
import { Breakpoints } from '../types';
3+
4+
export interface IWindowInfoContext {
5+
width: number,
6+
height: number,
7+
'--vw': string,
8+
'--vh': string,
9+
breakpoints: Breakpoints,
10+
eventsFired: number,
11+
}
312

413
const WindowInfoContext = createContext<IWindowInfoContext>({} as IWindowInfoContext);
514

src/WindowInfoContext/types.ts

Lines changed: 0 additions & 17 deletions
This file was deleted.

src/WindowInfoProvider/index.tsx

Lines changed: 133 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -1,119 +1,139 @@
1-
import React, { Component } from 'react';
2-
import WindowInfoContext from '../WindowInfoContext';
3-
import { IWindowInfoContext } from '../WindowInfoContext/types';
4-
import { Props } from './types';
5-
6-
class WindowInfoProvider extends Component<Props, IWindowInfoContext> {
7-
constructor(props: Props) {
8-
super(props);
9-
10-
this.state = {
11-
width: 0,
12-
height: 0,
13-
'--vw': '0px',
14-
'--vh': '0px',
15-
breakpoints: {
16-
xs: false,
17-
s: false,
18-
m: false,
19-
l: false,
20-
xl: false,
21-
},
22-
eventsFired: 0,
23-
animationScheduled: false,
24-
};
25-
}
26-
27-
componentDidMount(): void {
28-
window.addEventListener('resize', this.requestAnimation);
29-
window.addEventListener('orientationchange', this.updateWindowInfoWithTimeout);
30-
this.updateWindowInfo();
31-
}
32-
33-
componentWillUnmount(): void {
34-
window.removeEventListener('resize', this.requestAnimation);
35-
window.removeEventListener('orientationchange', this.updateWindowInfoWithTimeout);
36-
}
1+
import React, { useCallback, useEffect, useReducer, useRef } from 'react';
2+
import { Breakpoints } from '../types';
3+
import WindowInfoContext, { IWindowInfoContext } from '../WindowInfoContext';
4+
5+
const reducer = (
6+
state: IWindowInfoContext,
7+
payload: {
8+
breakpoints: Breakpoints,
9+
animationRef: React.MutableRefObject<number>
10+
},
11+
): IWindowInfoContext => {
12+
const {
13+
breakpoints,
14+
animationRef,
15+
} = payload;
16+
17+
animationRef.current = undefined;
18+
19+
const {
20+
eventsFired: prevEventsFired,
21+
} = state;
22+
23+
const {
24+
documentElement: {
25+
style,
26+
clientWidth,
27+
clientHeight,
28+
},
29+
} = document;
30+
31+
const {
32+
innerWidth: windowWidth,
33+
innerHeight: windowHeight,
34+
} = window;
35+
36+
const viewportWidth = `${clientWidth / 100}px`;
37+
const viewportHeight = `${clientHeight / 100}px`;
38+
39+
const newState = {
40+
width: windowWidth,
41+
height: windowHeight,
42+
'--vw': viewportWidth,
43+
'--vh': viewportHeight,
44+
breakpoints: Object.keys(breakpoints).reduce((matchMediaBreakpoints, key) => ({
45+
...matchMediaBreakpoints,
46+
[key]: window.matchMedia(breakpoints[key]).matches,
47+
}), {}),
48+
eventsFired: prevEventsFired + 1,
49+
};
50+
51+
// This method is a cross-browser patch to achieve above-the-fold, fullscreen mobile experiences.
52+
// The technique accounts for the collapsing bottom toolbar of some mobile browsers which are out of normal flow.
53+
// It provides an alternate to the "vw" and "vh" CSS units by generating respective CSS variables.
54+
// It specifically reads the size of documentElement since its height does not include the toolbar.
55+
style.setProperty('--vw', viewportWidth);
56+
style.setProperty('--vh', viewportHeight);
57+
58+
return newState;
59+
};
60+
61+
const WindowInfoProvider: React.FC<{
62+
breakpoints: Breakpoints
63+
}> = (props) => {
64+
const {
65+
breakpoints,
66+
children,
67+
} = props;
68+
69+
const animationRef = useRef<number>(null);
70+
71+
const [state, dispatch] = useReducer(reducer, {
72+
width: undefined,
73+
height: undefined,
74+
'--vw': '',
75+
'--vh': '',
76+
breakpoints: undefined,
77+
eventsFired: 0,
78+
});
79+
80+
const requestAnimation = useCallback((): void => {
81+
if (animationRef.current) cancelAnimationFrame(animationRef.current);
82+
animationRef.current = requestAnimationFrame(
83+
() => dispatch({
84+
breakpoints,
85+
animationRef,
86+
}),
87+
);
88+
}, [breakpoints]);
3789

38-
updateWindowInfoWithTimeout = (): void => {
90+
const requestThrottledAnimation = useCallback((): void => {
3991
setTimeout(() => {
40-
this.requestAnimation();
92+
requestAnimation();
4193
}, 500);
42-
}
43-
44-
requestAnimation = (): void => {
45-
const { animationScheduled } = this.state;
46-
if (!animationScheduled) {
47-
this.setState({
48-
animationScheduled: true,
49-
}, () => requestAnimationFrame(this.updateWindowInfo));
94+
}, [requestAnimation]);
95+
96+
useEffect(() => {
97+
window.addEventListener('resize', requestAnimation);
98+
window.addEventListener('orientationchange', requestThrottledAnimation);
99+
100+
return () => {
101+
window.removeEventListener('resize', requestAnimation);
102+
window.removeEventListener('orientationchange', requestThrottledAnimation);
103+
};
104+
}, [
105+
requestAnimation,
106+
requestThrottledAnimation,
107+
]);
108+
109+
// use this effect to test rAF debounce by requesting animation every 1ms, for a total 120ms
110+
// results: ~23 requests will be canceled, ~17 requests will be canceled, and only ~8 will truly dispatch
111+
// useEffect(() => {
112+
// const firstID = setInterval(requestAnimation, 1);
113+
// setInterval(() => clearInterval(firstID), 120);
114+
// }, [requestAnimation]);
115+
116+
useEffect(() => {
117+
if (state.eventsFired === 0) {
118+
dispatch({
119+
breakpoints,
120+
animationRef,
121+
});
50122
}
51-
}
52-
53-
updateWindowInfo = (): void => {
54-
const {
55-
breakpoints: {
56-
xs,
57-
s,
58-
m,
59-
l,
60-
xl,
61-
} = {},
62-
} = this.props;
63-
64-
const { eventsFired: prevEventsFired } = this.state;
65-
66-
const {
67-
documentElement: {
68-
style,
69-
clientWidth,
70-
clientHeight,
71-
},
72-
} = document;
73-
74-
const {
75-
innerWidth: windowWidth,
76-
innerHeight: windowHeight,
77-
} = window;
78-
79-
const viewportWidth = `${clientWidth / 100}px`;
80-
const viewportHeight = `${clientHeight / 100}px`;
81-
82-
this.setState({
83-
width: windowWidth,
84-
height: windowHeight,
85-
'--vw': viewportWidth,
86-
'--vh': viewportHeight,
87-
breakpoints: {
88-
xs: window.matchMedia(`(max-width: ${xs}px)`).matches,
89-
s: window.matchMedia(`(max-width: ${s}px)`).matches,
90-
m: window.matchMedia(`(max-width: ${m}px)`).matches,
91-
l: window.matchMedia(`(max-width: ${l}px)`).matches,
92-
xl: window.matchMedia(`(max-width: ${xl}px)`).matches,
93-
},
94-
eventsFired: prevEventsFired + 1,
95-
animationScheduled: false,
96-
});
97-
98-
// This method is a cross-browser patch to achieve above-the-fold, fullscreen mobile experiences.
99-
// The technique accounts for the collapsing bottom toolbar of some mobile browsers which are out of normal flow.
100-
// It provides an alternate to the "vw" and "vh" CSS units by generating respective CSS variables.
101-
// It specifically reads the size of documentElement since its height does not include the toolbar.
102-
style.setProperty('--vw', viewportWidth);
103-
style.setProperty('--vh', viewportHeight);
104-
}
105-
106-
render(): JSX.Element {
107-
const { children } = this.props;
108-
const windowInfo = { ...this.state };
109-
delete windowInfo.animationScheduled;
110-
111-
return (
112-
<WindowInfoContext.Provider value={{ ...windowInfo }}>
113-
{children && children}
114-
</WindowInfoContext.Provider>
115-
);
116-
}
117-
}
123+
}, [
124+
breakpoints,
125+
state,
126+
]);
127+
128+
return (
129+
<WindowInfoContext.Provider
130+
value={{
131+
...state,
132+
}}
133+
>
134+
{children && children}
135+
</WindowInfoContext.Provider>
136+
);
137+
};
118138

119139
export default WindowInfoProvider;

src/WindowInfoProvider/types.ts

Lines changed: 0 additions & 11 deletions
This file was deleted.

0 commit comments

Comments
 (0)