Skip to content

Commit

Permalink
feat(DeviceSize): Responsive rendering components
Browse files Browse the repository at this point in the history
  • Loading branch information
Oscar Martinez authored and ooHmartY committed Jun 14, 2018
1 parent 1259f3c commit f64e612
Show file tree
Hide file tree
Showing 9 changed files with 288 additions and 34 deletions.
3 changes: 3 additions & 0 deletions src/components/DeviceSize/Context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import React from "react";

export const { Provider, Consumer } = React.createContext({ isSmall: true });
85 changes: 85 additions & 0 deletions src/components/DeviceSize/Provider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/* global window */
import React from "react";
import PropTypes from "prop-types";

import { Provider } from "./Context";
import constants from "../../theme/constants";

export default class DeviceSizeProvider extends React.Component {
static propTypes = {
children: PropTypes.node.isRequired,
fallbackDetection: PropTypes.func,
cssOnly: PropTypes.bool
};

static defaultProps = { fallbackDetection: null, cssOnly: false };

initialState = { isSmall: true }; // eslint-disable-line

state = this.initialState;

componentDidMount() {
if (
typeof window === "undefined" ||
typeof window.matchMedia !== "function"
)
return;

if (this.props.cssOnly) {
this.setSize();
return;
}

this.smallMedia = window.matchMedia(constants.breakpoints.small);
this.mediumMedia = window.matchMedia(constants.breakpoints.medium);
this.largeMedia = window.matchMedia(constants.breakpoints.large);
this.xLargeMedia = window.matchMedia(constants.breakpoints.xLarge);
this.smallMedia.addListener(this.setSize);
this.mediumMedia.addListener(this.setSize);
this.largeMedia.addListener(this.setSize);
this.xLargeMedia.addListener(this.setSize);
this.setSize();
}

componentDidUpdate() {
if (!this.props.cssOnly) return;

this.unsubscribe();
}

componentWillUnmount() {
this.unsubscribe();
}

setSize = () => {
if (this.props.cssOnly) {
this.setState(() => ({ cssOnly: true }));
return;
}

this.setState(() => ({
isSmall: this.smallMedia.matches,
isMedium: this.mediumMedia.matches && !this.largeMedia.matches,
isLarge: this.largeMedia.matches && !this.xLargeMedia.matches,
isXLarge: this.xLargeMedia.matches,
cssOnly: false
}));
};

unsubscribe = () => {
if (this.smallMedia) this.smallMedia.removeListener(this.setSize);
if (this.mediumMedia) this.mediumMedia.removeListener(this.setSize);
if (this.largeMedia) this.largeMedia.removeListener(this.setSize);
if (this.xLargeMedia) this.xLargeMedia.removeListener(this.setSize);
};

render() {
const { fallbackDetection } = this.props;
const val = fallbackDetection ? fallbackDetection() : this.state;
return (
<Provider value={val || this.initialState}>
{this.props.children}
</Provider>
);
}
}
79 changes: 79 additions & 0 deletions src/components/DeviceSize/__test__/Provider.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/* global window */
import React from "react";
import { render } from "react-testing-library";

import Provider from "../Provider";
import { Consumer } from "../Context";

describe("DeviceSize", () => {
describe("with fallbacks", () => {
it("calls fallback function", () => {
const fallbackDetection = jest.fn();
render(
<Provider fallbackDetection={fallbackDetection}>
<Consumer>{() => "content"}</Consumer>
</Provider>
);

expect(fallbackDetection).toHaveBeenCalled();
});
});

describe("with matchMedia", () => {
const addListener = jest.fn();
const removeListener = jest.fn();
const matchMedia = jest.fn(() => ({ addListener, removeListener }));
beforeEach(() => {
matchMedia.mockClear();
addListener.mockClear();
removeListener.mockClear();

Object.defineProperty(window, "matchMedia", {
writable: true,
value: matchMedia
});
});

it("adds mediaqueries to matchMedia", () => {
render(
<Provider>
<Consumer>{() => "content"}</Consumer>
</Provider>
);

expect(matchMedia).toHaveBeenCalled();
});

it("does not add listeners when cssOnly is true", () => {
render(
<Provider cssOnly>
<Consumer>{() => "content"}</Consumer>
</Provider>
);

expect(matchMedia).not.toHaveBeenCalled();
});

it("adds listeners to matchMedia", () => {
render(
<Provider>
<Consumer>{() => "content"}</Consumer>
</Provider>
);

expect(addListener).toHaveBeenCalled();
});

it("cleans up when unmounted", () => {
const { unmount } = render(
<Provider>
<Consumer>{() => "content"}</Consumer>
</Provider>
);

unmount();

expect(removeListener).toHaveBeenCalled();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`DisplayFor cssOnly renders custom display prop 1`] = `
<div
class="sc-bdVaJa fgvBx"
display="flex"
>
Content
</div>
`;

exports[`DisplayFor cssOnly renders nothing when value is null 1`] = `
<div
class="sc-bdVaJa gPRijU"
display="block"
>
Content
</div>
`;
27 changes: 27 additions & 0 deletions src/components/DeviceSize/__test__/index.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from "react";
import { render } from "react-testing-library";
import DisplayFor from "../";

jest.mock("../Context", () => ({
Consumer: ({ children }) => children({ cssOnly: true })
}));

describe("DisplayFor", () => {
describe("cssOnly", () => {
it("renders nothing when value is null", () => {
const { container } = render(<DisplayFor small>Content</DisplayFor>);

expect(container.firstChild).toMatchSnapshot();
});

it("renders custom display prop", () => {
const { container } = render(
<DisplayFor display="flex" medium large xLarge>
Content
</DisplayFor>
);

expect(container.firstChild).toMatchSnapshot();
});
});
});
65 changes: 65 additions & 0 deletions src/components/DeviceSize/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React from "react";
import styled from "styled-components";
import PropTypes from "prop-types";
import { Consumer } from "./Context";
import { mediumAndUp, largeAndUp, xLargeAndUp } from "../../theme/mediaQueries";

const StyledVisibility = styled.div`
display: ${props => (props.small ? props.display : "none")};
${mediumAndUp`
display: ${props => (props.medium ? props.display : "none")}
`};
${largeAndUp`
display: ${props => (props.large ? props.display : "none")}
`};
${xLargeAndUp`
display: ${props => (props.xLarge ? props.display : "none")}
`};
`;

export default class DisplayFor extends React.Component {
static propTypes = {
small: PropTypes.bool,
medium: PropTypes.bool,
large: PropTypes.bool,
xLarge: PropTypes.bool,
display: PropTypes.oneOf(["block", "inline-block", "flex"]),
children: PropTypes.node,
className: PropTypes.string
};

static defaultProps = {
display: "block",
small: false,
medium: false,
large: false,
xLarge: false,
children: null,
className: null
};

render() {
const { small, medium, large, xLarge, children } = this.props;
return (
<Consumer>
{val => {
/* istanbul ignore next */
if (val.cssOnly) {
return <StyledVisibility {...this.props} />;
}

/* istanbul ignore next */
if (val.isSmall && small) return children;
/* istanbul ignore next */
if (val.isMedium && medium) return children;
/* istanbul ignore next */
if (val.isLarge && large) return children;
/* istanbul ignore next */
if (val.isXLarge && xLarge) return children;
/* istanbul ignore next */
return null;
}}
</Consumer>
);
}
}
33 changes: 0 additions & 33 deletions src/components/Visibility.js

This file was deleted.

7 changes: 6 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ export {
UserIcon,
StarIcon
} from "./components/Icons";
export { default as Visibility } from "./components/Visibility";
export {
default as DeviceSizeProvider
} from "./components/DeviceSize/Provider";
export {
Consumer as DeviceSizeConsumer
} from "./components/DeviceSize/Context";
export { default as colors } from "./theme/colors";
export { default as constants } from "./theme/constants";
export { default as spacing } from "./theme/spacing";
Expand Down
4 changes: 4 additions & 0 deletions src/theme/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ const constants = {
large: "4px"
},
breakpoints: {
small: "(max-width: 767px)",
medium: "(min-width: 768px)",
large: "(min-width: 1024px)",
xLarge: "(min-width: 1440px)",
mediumAndUp: "(min-width: 481px)",
largeAndUp: "(min-width: 769px)",
xLargeAndUp: "(min-width: 1025px)"
Expand Down

0 comments on commit f64e612

Please sign in to comment.