Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow style rules subsetting #140

Merged
merged 1 commit into from Jul 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 4 additions & 3 deletions README.md
Expand Up @@ -426,8 +426,9 @@ export const HomePage = () => {
Besides the `Media` and `MediaContextProvider` components, there's a
`createMediaStyle` function that produces the CSS styling for all possible media
queries that the `Media` instance can make use of while markup is being passed
from the server to the client during hydration. Be sure to insert this within a
`<style>` tag
from the server to the client during hydration. If only a subset of breakpoint
keys is used those can be optional specified as a parameter to minimize the
output. Be sure to insert this within a `<style>` tag
[in your document’s `<head>`](https://github.com/artsy/fresnel/blob/master/examples/ssr-rendering/src/server.tsx#L28).

It’s advisable to do this setup in its own module so that it can be easily
Expand All @@ -446,7 +447,7 @@ const ExampleAppMedia = createMedia({
})

// Generate CSS to be injected into the head
export const mediaStyle = ExampleAppMedia.createMediaStyle()
export const mediaStyle = ExampleAppMedia.createMediaStyle() // optional: .createMediaStyle(['at'])
export const { Media, MediaContextProvider } = ExampleAppMedia
```

Expand Down
62 changes: 42 additions & 20 deletions src/Breakpoints.ts
Expand Up @@ -14,24 +14,32 @@ function breakpointKey(breakpoint: string | Tuple) {
return Array.isArray(breakpoint) ? breakpoint.join("-") : breakpoint
}

export enum BreakpointKey {
at = "at",
lessThan = "lessThan",
greaterThan = "greaterThan",
greaterThanOrEqual = "greaterThanOrEqual",
between = "between",
}

/**
* Encapsulates all breakpoint data needed by the Media component. The data is
* generated on initialization so no further runtime work is necessary.
*/
export class Breakpoints<B extends string> {
static validKeys() {
return ["at", "lessThan", "greaterThan", "greaterThanOrEqual", "between"]
return [
BreakpointKey.at,
BreakpointKey.lessThan,
BreakpointKey.greaterThan,
BreakpointKey.greaterThanOrEqual,
BreakpointKey.between,
]
}

private _sortedBreakpoints: ReadonlyArray<string>
private _breakpoints: { [key: string]: number }
private _mediaQueries: {
at: Map<string, string>
lessThan: Map<string, string>
greaterThan: Map<string, string>
greaterThanOrEqual: Map<string, string>
between: Map<string, string>
}
private _breakpoints: Record<string, number>
private _mediaQueries: Record<BreakpointKey, Map<string, string>>

constructor(breakpoints: { [key: string]: number }) {
this._breakpoints = breakpoints
Expand All @@ -53,20 +61,26 @@ export class Breakpoints<B extends string> {
)

this._mediaQueries = {
at: this._createBreakpointQueries("at", this._sortedBreakpoints),
lessThan: this._createBreakpointQueries(
"lessThan",
[BreakpointKey.at]: this._createBreakpointQueries(
BreakpointKey.at,
this._sortedBreakpoints
),
[BreakpointKey.lessThan]: this._createBreakpointQueries(
BreakpointKey.lessThan,
this._sortedBreakpoints.slice(1)
),
greaterThan: this._createBreakpointQueries(
"greaterThan",
[BreakpointKey.greaterThan]: this._createBreakpointQueries(
BreakpointKey.greaterThan,
this._sortedBreakpoints.slice(0, -1)
),
greaterThanOrEqual: this._createBreakpointQueries(
"greaterThanOrEqual",
[BreakpointKey.greaterThanOrEqual]: this._createBreakpointQueries(
BreakpointKey.greaterThanOrEqual,
this._sortedBreakpoints
),
between: this._createBreakpointQueries("between", betweenCombinations),
[BreakpointKey.between]: this._createBreakpointQueries(
BreakpointKey.between,
betweenCombinations
),
}
}

Expand All @@ -75,7 +89,7 @@ export class Breakpoints<B extends string> {
}

public get dynamicResponsiveMediaQueries() {
return Array.from(this._mediaQueries.at.entries()).reduce(
return Array.from(this._mediaQueries[BreakpointKey.at].entries()).reduce(
(acc, [k, v]) => ({ ...acc, [k]: v }),
{}
)
Expand Down Expand Up @@ -118,8 +132,16 @@ export class Breakpoints<B extends string> {
}) as B | undefined
}

public toRuleSets() {
return Object.entries(this._mediaQueries).reduce(
public toRuleSets(keys = Breakpoints.validKeys()) {
const selectedMediaQueries = keys.reduce(
(mediaQueries, query) => {
mediaQueries[query] = this._mediaQueries[query]
return mediaQueries
},
{} as Record<BreakpointKey, Map<string, string>>
)

return Object.entries(selectedMediaQueries).reduce(
(acc: string[], [type, queries]) => {
queries.forEach((query, breakpoint) => {
// We need to invert the query, such that it matches when we want the
Expand Down
11 changes: 9 additions & 2 deletions src/Interactions.ts
@@ -1,12 +1,16 @@
import { createClassName, createRuleSet } from "./Utils"

export enum InteractionKey {
interaction = "interaction",
}

/**
* Encapsulates all interaction data needed by the Media component. The data is
* generated on initialization so no further runtime work is necessary.
*/
export class Interactions {
static validKeys() {
return ["interaction"]
return [InteractionKey.interaction]
}

private _interactions: { [key: string]: string }
Expand All @@ -20,7 +24,10 @@ export class Interactions {
(acc: string[], [name, query]) => {
return [
...acc,
createRuleSet(createClassName("interaction", name), query),
createRuleSet(
createClassName(InteractionKey.interaction, name),
query
),
]
},
[]
Expand Down
5 changes: 3 additions & 2 deletions src/Media.tsx
Expand Up @@ -4,6 +4,7 @@ import React from "react"
import { createResponsiveComponents } from "./DynamicResponsive"
import { MediaQueries } from "./MediaQueries"
import { intersection, propKey, createClassName } from "./Utils"
import { BreakpointKey } from "./Breakpoints"

/**
* A render prop that can be used to render a different container element than
Expand Down Expand Up @@ -240,7 +241,7 @@ export interface CreateMediaResults<B, I> {
* Generates a set of CSS rules that you should include in your application’s
* styling to enable the hiding behaviour of your `Media` component uses.
*/
createMediaStyle(): string
createMediaStyle(breakpointKeys?: BreakpointKey[]): string

/**
* A list of your application’s breakpoints sorted from small to large.
Expand Down Expand Up @@ -458,7 +459,7 @@ export function createMedia<
}
}

const MutuallyExclusiveProps = MediaQueries.validKeys()
const MutuallyExclusiveProps: string[] = MediaQueries.validKeys()

function validateProps(props) {
const selectedProps = Object.keys(props).filter(prop =>
Expand Down
6 changes: 3 additions & 3 deletions src/MediaQueries.ts
@@ -1,4 +1,4 @@
import { Breakpoints } from "./Breakpoints"
import { Breakpoints, BreakpointKey } from "./Breakpoints"
import { Interactions } from "./Interactions"
import { intersection } from "./Utils"
import { MediaBreakpointProps } from "./Media"
Expand Down Expand Up @@ -28,11 +28,11 @@ export class MediaQueries<B extends string> {
return this._breakpoints
}

public toStyle = () => {
public toStyle = (breakpointKeys?: BreakpointKey[]) => {
return [
// Don’t add any size to the layout
".fresnel-container{margin:0;padding:0;}",
...this._breakpoints.toRuleSets(),
...this._breakpoints.toRuleSets(breakpointKeys),
...this._interactions.toRuleSets(),
].join("\n")
}
Expand Down
13 changes: 13 additions & 0 deletions src/__test__/Media.test.tsx
Expand Up @@ -4,6 +4,7 @@ import React from "react"
import renderer, { ReactTestRendererJSON } from "react-test-renderer"
import { injectGlobal } from "styled-components"
import { createMedia } from "../Media"
import { BreakpointKey } from "../Breakpoints"
import { MediaQueries } from "../MediaQueries"
import ReactDOMServer from "react-dom/server"
import ReactDOM from "react-dom"
Expand Down Expand Up @@ -145,6 +146,18 @@ describe("Media", () => {
.toJSON()
expect(query.props.className).toContain("foo")
})

it("includes only style rules for specified breakpoint keys", () => {
const defaultMediaStyles = createMediaStyle()
expect(defaultMediaStyles).toContain(".fresnel-between-small-large")

const subsetMediaStyles = createMediaStyle([
BreakpointKey.at,
BreakpointKey.greaterThan,
])
expect(subsetMediaStyles).not.toContain(".fresnel-between-small-large")
expect(subsetMediaStyles).toContain(".fresnel-at-extra-small")
})
})

describe("concerning breakpoints", () => {
Expand Down
1 change: 1 addition & 0 deletions src/index.tsx
@@ -1 +1,2 @@
export { createMedia } from "./Media"
export { BreakpointKey } from "./Breakpoints"