Skip to content

Commit

Permalink
Merge pull request #18 from faceless-ui/feature/custom-breakpoints
Browse files Browse the repository at this point in the history
customizable breakpoints
  • Loading branch information
jacobsfletch authored May 7, 2021
2 parents 2830715 + 37f3ad4 commit ac11673
Show file tree
Hide file tree
Showing 12 changed files with 233 additions and 190 deletions.
16 changes: 11 additions & 5 deletions demo/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,17 @@ import UseWindowInfo from './UseWindowInfo.demo';
// import LogProps from './LogProps';

const breakpoints = {
xs: 350,
s: 576,
m: 850,
l: 992,
xl: 1200,
'mobile-first-xs': '(min-width: 350px)',
'mobile-first-s': '(min-width: 576px)',
'mobile-first-m': '(min-width: 850px)',
'mobile-first-l': '(min-width: 992px)',
'mobile-first-xl': '(min-width: 1200px)',

'desktop-first-xs': '(max-width: 350px)',
'desktop-first-s': '(max-width: 576px)',
'desktop-first-m': '(max-width: 850px)',
'desktop-first-l': '(max-width: 992px)',
'desktop-first-xl': '(max-width: 1200px)',
};

const AppDemo: React.FC = () => (
Expand Down
2 changes: 1 addition & 1 deletion demo/LogProps.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { IWindowInfoContext } from '../src/WindowInfoContext/types';
import { IWindowInfoContext } from '../src/WindowInfoContext';

const filterObject = () => {
const seen = new WeakSet();
Expand Down
34 changes: 11 additions & 23 deletions demo/Stylesheet.demo.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import React, { Fragment } from 'react';
import { Breakpoints } from '../src/types';

type Props = {
breakpoints: {
xs: number,
s: number,
m: number,
l: number,
xl: number
}
breakpoints: Breakpoints
}

const StylesheetDemo: React.FC<Props> = (props) => {
Expand All @@ -22,27 +17,20 @@ const StylesheetDemo: React.FC<Props> = (props) => {
<Fragment>
<style
dangerouslySetInnerHTML={{
__html: hasBreakpoints && breakpointsKeys.map((key) => `@media(max-width: ${breakpoints[key]}px) { #${key} { color: green; } }`).join(' '),
__html: hasBreakpoints && breakpointsKeys.map((key) => `@media${breakpoints[key]} { #${key} { color: green; } }`).join(' '),
}}
/>
<code>
<pre>
@media:
<div id="xs">
xs
</div>
<div id="s">
s
</div>
<div id="m">
m
</div>
<div id="l">
l
</div>
<div id="xl">
xl
</div>
{hasBreakpoints && breakpointsKeys.map((breakpointKey) => (
<div
key={breakpointKey}
id={breakpointKey}
>
{`${breakpointKey}: ${breakpoints[breakpointKey]}`}
</div>
))}
</pre>
</code>
</Fragment>
Expand Down
2 changes: 1 addition & 1 deletion demo/WithWindowInfo.demo.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { withWindowInfo } from '../src'; // swap '../src' for '../dist/build.bundle' to demo production build
import { IWindowInfoContext } from '../src/WindowInfoContext/types';
import { IWindowInfoContext } from '../src/WindowInfoContext';
import LogProps from './LogProps';

type Props = {
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@
"@types/node": "^14.14.22",
"@types/react": "^17.0.0",
"@trbl/utils": "^1.1.1",
"@typescript-eslint/eslint-plugin": "^4.11.1",
"@typescript-eslint/parser": "^4.11.1",
"@typescript-eslint/eslint-plugin": "^4.22.1",
"@typescript-eslint/parser": "^4.22.1",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.2",
"eslint": "^7.16.0",
Expand Down
11 changes: 10 additions & 1 deletion src/WindowInfoContext/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import { createContext } from 'react';
import { IWindowInfoContext } from './types';
import { Breakpoints } from '../types';

export interface IWindowInfoContext {
width: number,
height: number,
'--vw': string,
'--vh': string,
breakpoints: Breakpoints,
eventsFired: number,
}

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

Expand Down
17 changes: 0 additions & 17 deletions src/WindowInfoContext/types.ts

This file was deleted.

246 changes: 133 additions & 113 deletions src/WindowInfoProvider/index.tsx
Original file line number Diff line number Diff line change
@@ -1,119 +1,139 @@
import React, { Component } from 'react';
import WindowInfoContext from '../WindowInfoContext';
import { IWindowInfoContext } from '../WindowInfoContext/types';
import { Props } from './types';

class WindowInfoProvider extends Component<Props, IWindowInfoContext> {
constructor(props: Props) {
super(props);

this.state = {
width: 0,
height: 0,
'--vw': '0px',
'--vh': '0px',
breakpoints: {
xs: false,
s: false,
m: false,
l: false,
xl: false,
},
eventsFired: 0,
animationScheduled: false,
};
}

componentDidMount(): void {
window.addEventListener('resize', this.requestAnimation);
window.addEventListener('orientationchange', this.updateWindowInfoWithTimeout);
this.updateWindowInfo();
}

componentWillUnmount(): void {
window.removeEventListener('resize', this.requestAnimation);
window.removeEventListener('orientationchange', this.updateWindowInfoWithTimeout);
}
import React, { useCallback, useEffect, useReducer, useRef } from 'react';
import { Breakpoints } from '../types';
import WindowInfoContext, { IWindowInfoContext } from '../WindowInfoContext';

const reducer = (
state: IWindowInfoContext,
payload: {
breakpoints: Breakpoints,
animationRef: React.MutableRefObject<number>
},
): IWindowInfoContext => {
const {
breakpoints,
animationRef,
} = payload;

animationRef.current = undefined;

const {
eventsFired: prevEventsFired,
} = state;

const {
documentElement: {
style,
clientWidth,
clientHeight,
},
} = document;

const {
innerWidth: windowWidth,
innerHeight: windowHeight,
} = window;

const viewportWidth = `${clientWidth / 100}px`;
const viewportHeight = `${clientHeight / 100}px`;

const newState = {
width: windowWidth,
height: windowHeight,
'--vw': viewportWidth,
'--vh': viewportHeight,
breakpoints: Object.keys(breakpoints).reduce((matchMediaBreakpoints, key) => ({
...matchMediaBreakpoints,
[key]: window.matchMedia(breakpoints[key]).matches,
}), {}),
eventsFired: prevEventsFired + 1,
};

// This method is a cross-browser patch to achieve above-the-fold, fullscreen mobile experiences.
// The technique accounts for the collapsing bottom toolbar of some mobile browsers which are out of normal flow.
// It provides an alternate to the "vw" and "vh" CSS units by generating respective CSS variables.
// It specifically reads the size of documentElement since its height does not include the toolbar.
style.setProperty('--vw', viewportWidth);
style.setProperty('--vh', viewportHeight);

return newState;
};

const WindowInfoProvider: React.FC<{
breakpoints: Breakpoints
}> = (props) => {
const {
breakpoints,
children,
} = props;

const animationRef = useRef<number>(null);

const [state, dispatch] = useReducer(reducer, {
width: undefined,
height: undefined,
'--vw': '',
'--vh': '',
breakpoints: undefined,
eventsFired: 0,
});

const requestAnimation = useCallback((): void => {
if (animationRef.current) cancelAnimationFrame(animationRef.current);
animationRef.current = requestAnimationFrame(
() => dispatch({
breakpoints,
animationRef,
}),
);
}, [breakpoints]);

updateWindowInfoWithTimeout = (): void => {
const requestThrottledAnimation = useCallback((): void => {
setTimeout(() => {
this.requestAnimation();
requestAnimation();
}, 500);
}

requestAnimation = (): void => {
const { animationScheduled } = this.state;
if (!animationScheduled) {
this.setState({
animationScheduled: true,
}, () => requestAnimationFrame(this.updateWindowInfo));
}, [requestAnimation]);

useEffect(() => {
window.addEventListener('resize', requestAnimation);
window.addEventListener('orientationchange', requestThrottledAnimation);

return () => {
window.removeEventListener('resize', requestAnimation);
window.removeEventListener('orientationchange', requestThrottledAnimation);
};
}, [
requestAnimation,
requestThrottledAnimation,
]);

// use this effect to test rAF debounce by requesting animation every 1ms, for a total 120ms
// results: ~23 requests will be canceled, ~17 requests will be canceled, and only ~8 will truly dispatch
// useEffect(() => {
// const firstID = setInterval(requestAnimation, 1);
// setInterval(() => clearInterval(firstID), 120);
// }, [requestAnimation]);

useEffect(() => {
if (state.eventsFired === 0) {
dispatch({
breakpoints,
animationRef,
});
}
}

updateWindowInfo = (): void => {
const {
breakpoints: {
xs,
s,
m,
l,
xl,
} = {},
} = this.props;

const { eventsFired: prevEventsFired } = this.state;

const {
documentElement: {
style,
clientWidth,
clientHeight,
},
} = document;

const {
innerWidth: windowWidth,
innerHeight: windowHeight,
} = window;

const viewportWidth = `${clientWidth / 100}px`;
const viewportHeight = `${clientHeight / 100}px`;

this.setState({
width: windowWidth,
height: windowHeight,
'--vw': viewportWidth,
'--vh': viewportHeight,
breakpoints: {
xs: window.matchMedia(`(max-width: ${xs}px)`).matches,
s: window.matchMedia(`(max-width: ${s}px)`).matches,
m: window.matchMedia(`(max-width: ${m}px)`).matches,
l: window.matchMedia(`(max-width: ${l}px)`).matches,
xl: window.matchMedia(`(max-width: ${xl}px)`).matches,
},
eventsFired: prevEventsFired + 1,
animationScheduled: false,
});

// This method is a cross-browser patch to achieve above-the-fold, fullscreen mobile experiences.
// The technique accounts for the collapsing bottom toolbar of some mobile browsers which are out of normal flow.
// It provides an alternate to the "vw" and "vh" CSS units by generating respective CSS variables.
// It specifically reads the size of documentElement since its height does not include the toolbar.
style.setProperty('--vw', viewportWidth);
style.setProperty('--vh', viewportHeight);
}

render(): JSX.Element {
const { children } = this.props;
const windowInfo = { ...this.state };
delete windowInfo.animationScheduled;

return (
<WindowInfoContext.Provider value={{ ...windowInfo }}>
{children && children}
</WindowInfoContext.Provider>
);
}
}
}, [
breakpoints,
state,
]);

return (
<WindowInfoContext.Provider
value={{
...state,
}}
>
{children && children}
</WindowInfoContext.Provider>
);
};

export default WindowInfoProvider;
11 changes: 0 additions & 11 deletions src/WindowInfoProvider/types.ts

This file was deleted.

Loading

0 comments on commit ac11673

Please sign in to comment.