From 801545b821bb253a913cef1388c1ba136c554aa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eloy=20Dur=C3=A1n?= Date: Fri, 30 Nov 2018 19:54:40 +0100 Subject: [PATCH 1/3] [Media] Add more helpers to aid finding breakpoints to SSR. --- src/Breakpoints.ts | 35 +++++++++++++++++++++++++++++++---- src/Media.tsx | 12 +++++++++--- src/__test__/Media.test.tsx | 26 +++++++++++++++++--------- 3 files changed, 57 insertions(+), 16 deletions(-) diff --git a/src/Breakpoints.ts b/src/Breakpoints.ts index 017032c..c58bcf7 100644 --- a/src/Breakpoints.ts +++ b/src/Breakpoints.ts @@ -85,10 +85,37 @@ export class Breakpoints { return this._sortedBreakpoints[this._sortedBreakpoints.length - 1] } - public findBreakpointsForWidth = (width: number) => { - return this._sortedBreakpoints.filter( - breakpoint => width >= this._breakpoints[breakpoint] - ) as B[] + public findBreakpointsForWidths = ( + fromWidth: number, + throughWidth: number + ) => { + const fromBreakpoint = this.findBreakpointAtWidth(fromWidth) + if (!fromBreakpoint) { + return undefined + } + const throughBreakpoint = this.findBreakpointAtWidth(throughWidth) + if (!throughBreakpoint || fromBreakpoint === throughBreakpoint) { + return [fromBreakpoint] as B[] + } else { + return this._sortedBreakpoints.slice( + this._sortedBreakpoints.indexOf(fromBreakpoint), + this._sortedBreakpoints.indexOf(throughBreakpoint) + 1 + ) as B[] + } + } + + public findBreakpointAtWidth = (width: number) => { + return this._sortedBreakpoints.find((breakpoint, i) => { + const nextBreakpoint = this._sortedBreakpoints[i + 1] + if (nextBreakpoint) { + return ( + width >= this._breakpoints[breakpoint] && + width < this._breakpoints[nextBreakpoint] + ) + } else { + return width >= this._breakpoints[breakpoint] + } + }) as B | undefined } public toRuleSets() { diff --git a/src/Media.tsx b/src/Media.tsx index 6d8d103..c2e5bc1 100644 --- a/src/Media.tsx +++ b/src/Media.tsx @@ -244,9 +244,14 @@ export interface CreateMediaResults { /** * Creates a list of your application’s breakpoints that support the given - * width. + * widths and everything in between. */ - findBreakpointsForWidth(width: number): B[] + findBreakpointsForWidths(fromWidth: number, throughWidth: number): B[] + + /** + * Finds the breakpoint that matches the given width. + */ + findBreakpointAtWidth(width: number): B | undefined /** * Maps a list of values for various breakpoints to props that can be used @@ -429,7 +434,8 @@ export function createMedia< MediaContextProvider, createMediaStyle: mediaQueries.toStyle, SortedBreakpoints: [...mediaQueries.breakpoints.sortedBreakpoints], - findBreakpointsForWidth: mediaQueries.breakpoints.findBreakpointsForWidth, + findBreakpointAtWidth: mediaQueries.breakpoints.findBreakpointAtWidth, + findBreakpointsForWidths: mediaQueries.breakpoints.findBreakpointsForWidths, valuesWithBreakpointProps: mediaQueries.breakpoints.valuesWithBreakpointProps, } diff --git a/src/__test__/Media.test.tsx b/src/__test__/Media.test.tsx index 54346d2..6e9d8d0 100644 --- a/src/__test__/Media.test.tsx +++ b/src/__test__/Media.test.tsx @@ -23,7 +23,8 @@ const { MediaContextProvider, createMediaStyle, SortedBreakpoints, - findBreakpointsForWidth, + findBreakpointAtWidth, + findBreakpointsForWidths, valuesWithBreakpointProps, } = createMedia(config) @@ -39,18 +40,25 @@ describe("utilities", () => { ]) }) - it("returns a list of breakpoints that support the given width", () => { - expect(findBreakpointsForWidth(-42)).toEqual([]) - expect(findBreakpointsForWidth(42)).toEqual(["extra-small"]) - expect(findBreakpointsForWidth(767)).toEqual(["extra-small"]) - expect(findBreakpointsForWidth(768)).toEqual(["extra-small", "small"]) - expect(findBreakpointsForWidth(1042)).toEqual([ + it("returns the breakpoint that supports the given width", () => { + expect(findBreakpointAtWidth(-42)).toEqual(undefined) + expect(findBreakpointAtWidth(42)).toEqual("extra-small") + expect(findBreakpointAtWidth(767)).toEqual("extra-small") + expect(findBreakpointAtWidth(768)).toEqual("small") + expect(findBreakpointAtWidth(1042)).toEqual("medium") + expect(findBreakpointAtWidth(9999)).toEqual("large") + }) + + it("returns the breakpoints from the first through the last given widths", () => { + expect(findBreakpointsForWidths(-42, -21)).toEqual(undefined) + expect(findBreakpointsForWidths(42, 767)).toEqual(["extra-small"]) + expect(findBreakpointsForWidths(42, 768)).toEqual(["extra-small", "small"]) + expect(findBreakpointsForWidths(42, 1042)).toEqual([ "extra-small", "small", "medium", ]) - expect(findBreakpointsForWidth(9999)).toEqual([ - "extra-small", + expect(findBreakpointsForWidths(768, 9999)).toEqual([ "small", "medium", "large", From c4ac7ee6b82364d86b122628f3113511275c9773 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eloy=20Dur=C3=A1n?= Date: Fri, 30 Nov 2018 20:08:03 +0100 Subject: [PATCH 2/3] [example] Use @artsy/detect-responsive-traits --- .vscode/settings.json | 5 +- example/app.tsx | 2 +- example/onlyMatchListForUserAgent.ts | 57 --------- example/server.tsx | 173 +++++++++++++++++++-------- example/setup.ts | 3 +- example/tsconfig.json | 7 ++ package.json | 3 + yarn.lock | 27 +++-- 8 files changed, 156 insertions(+), 121 deletions(-) delete mode 100644 example/onlyMatchListForUserAgent.ts create mode 100644 example/tsconfig.json diff --git a/.vscode/settings.json b/.vscode/settings.json index f9bfa4e..1278882 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -18,5 +18,8 @@ }, "tslint.autoFixOnSave": true, "typescript.tsdk": "./node_modules/typescript/lib", - "debug.node.autoAttach": "on" + "debug.node.autoAttach": "on", + "cSpell.words": [ + "resizable" + ] } diff --git a/example/app.tsx b/example/app.tsx index c93b458..948367d 100644 --- a/example/app.tsx +++ b/example/app.tsx @@ -35,7 +35,7 @@ const LargeStyle: CSSProperties = { } export const App: React.SFC = () => ( -
+

Default <div> container diff --git a/example/onlyMatchListForUserAgent.ts b/example/onlyMatchListForUserAgent.ts deleted file mode 100644 index f0182ce..0000000 --- a/example/onlyMatchListForUserAgent.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Notes: - * - Apple devices do not include model details in their user-agent other than - * `iPhone` or `iPad`, so we need to always render for the largest option. - */ - -import { findBreakpointsForWidth, SortedBreakpoints } from "./setup" - -const devices: Array< - [ - RegExp, - { - type: string - width: number - height: number - touch: boolean - } - ] -> = [ - // iPhone XS Max - [ - /iPhone/, - { - type: "iPhone", - width: 414, - height: 896, - touch: true, - }, - ], - // iPad Pro (12.9-inch) - [ - /iPad/, - { - type: "iPad", - width: 1024, - height: 1336, - touch: true, - }, - ], -] - -// TODO: Simplify this hideous typing. -export function onlyMatchListForUserAgent( - userAgent: string -): Array<"hover" | "notHover" | (typeof SortedBreakpoints)[0]> { - const match = devices.find(([regexp]) => regexp.test(userAgent)) - if (match) { - const device = match[1] - // We support rotation, so take longest dimension - const max = Math.max(device.width, device.height) - return [ - device.touch ? "notHover" : "hover", - ...findBreakpointsForWidth(max), - ] - } - return null -} diff --git a/example/server.tsx b/example/server.tsx index 6a047d7..cecbc24 100644 --- a/example/server.tsx +++ b/example/server.tsx @@ -2,67 +2,85 @@ import Webpack from "webpack" import WebpackDevServer from "webpack-dev-server" import webpackConfig from "./webpack.config" import express from "express" - import ReactDOMServer from "react-dom/server" import React from "react" +import chalk from "chalk" + +import { findDevice } from "@artsy/detect-responsive-traits" -import { createMediaStyle, MediaContextProvider, SSRStyleID } from "./setup" +import { + createMediaStyle, + findBreakpointsForWidths, + findBreakpointAtWidth, + MediaContextProvider, + SortedBreakpoints, + SSRStyleID, +} from "./setup" import { App } from "./app" -import { onlyMatchListForUserAgent } from "./onlyMatchListForUserAgent" const app = express() -app.get("/", (_req, res) => { - res.send(` - - - - - - - `) -}) +/** + * Find the breakpoints and interactions that the server should render + */ +function onlyMatchListForUserAgent(userAgent: string): OnlyMatchList { + const device = findDevice(userAgent) + if (!device) { + log(userAgent) + return null + } else { + const supportsHover = device.touch ? "notHover" : "hover" + const onlyMatch: OnlyMatchList = device.resizable + ? [ + supportsHover, + ...findBreakpointsForWidths(device.minWidth, device.maxWidth), + ] + : [ + supportsHover, + findBreakpointAtWidth(device.minWidth), + findBreakpointAtWidth(device.maxWidth), + ] + log(userAgent, onlyMatch, device.description) + return onlyMatch + } +} +/** + * Demonstrate server-side _only_ rendering + */ app.get("/ssr-only", (req, res) => { - const onlyMatch = onlyMatchListForUserAgent(req.header("User-Agent")) - res.send(` - - - - - - - + res.send( + template({ + includeCSS: true, + body: `
${ReactDOMServer.renderToString( - + )}
- - - `) + `, + }) + ) }) +/** + * Demonstrate server-side rendering _with_ client-side JS rehydration + */ app.get("/rehydration", (req, res) => { - const onlyMatch = onlyMatchListForUserAgent(req.header("User-Agent")) - res.send(` - - - - - - - + res.send( + template({ + includeCSS: true, + body: `
Loading…
${ReactDOMServer.renderToString( - + )} @@ -77,19 +95,19 @@ app.get("/rehydration", (req, res) => { document.getElementById("loading-indicator").remove(); }, 1000) - - - `) + `, + }) + ) }) +/** + * Demonstrate client-side JS _only_ rendering + */ app.get("/client-only", (_req, res) => { - res.send(` - - - - - - + res.send( + template({ + includeCSS: false, + body: `
Loading…
- - - `) + `, + }) + ) +}) + +/** + * Misc things that are not of interest for demonstrating react-responsive-media + */ + +// TODO: Simplify this hideous typing. +type OnlyMatchList = Array<"hover" | "notHover" | (typeof SortedBreakpoints)[0]> + +app.get("/", (_req, res) => { + res.send( + template({ + includeCSS: false, + body: ` + + `, + }) + ) }) +const template = ({ includeCSS, body }) => ` + + + + + + ${ + includeCSS + ? `` + : "" + } + + + ${body} + + +` + +function log(userAgent: string, onlyMatch?: string[], device?: string) { + // tslint:disable-next-line:no-console + console.log( + `Render: ${chalk.green(onlyMatch ? onlyMatch.join(", ") : "ALL")}\n` + + `Device: ${device ? chalk.green(device) : chalk.red("n/a")}\n` + + `User-Agent: ${chalk.yellow(userAgent)}\n` + ) +} + const compiler = Webpack(webpackConfig) const devServerOptions = Object.assign({}, webpackConfig.devServer, { stats: { diff --git a/example/setup.ts b/example/setup.ts index e821643..655213b 100644 --- a/example/setup.ts +++ b/example/setup.ts @@ -17,7 +17,8 @@ const ExampleAppMedia = createMedia({ export const Media = ExampleAppMedia.Media export const MediaContextProvider = ExampleAppMedia.MediaContextProvider export const createMediaStyle = ExampleAppMedia.createMediaStyle -export const findBreakpointsForWidth = ExampleAppMedia.findBreakpointsForWidth +export const findBreakpointsForWidths = ExampleAppMedia.findBreakpointsForWidths +export const findBreakpointAtWidth = ExampleAppMedia.findBreakpointAtWidth export const SortedBreakpoints = ExampleAppMedia.SortedBreakpoints export const SSRStyleID = "ssr-rrm-style" diff --git a/example/tsconfig.json b/example/tsconfig.json new file mode 100644 index 0000000..bc043cd --- /dev/null +++ b/example/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "rootDirs": ["../example", "../src"], + "resolveJsonModule": true + } +} diff --git a/package.json b/package.json index c1c7522..6a8a08b 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "react": "^16.3.0" }, "devDependencies": { + "@artsy/detect-responsive-traits": "^0.0.1", "@babel/cli": "^7.0.0", "@babel/core": "^7.0.0", "@babel/node": "^7.0.0", @@ -48,6 +49,7 @@ "@babel/preset-env": "^7.0.0", "@babel/preset-react": "^7.0.0", "@babel/preset-typescript": "^7.0.0", + "@types/chalk": "^2.2.0", "@types/express": "^4.16.0", "@types/jest": "^23.1.0", "@types/node": "^10.3.0", @@ -59,6 +61,7 @@ "babel-jest": "^23.0.1", "babel-loader": "^8.0.4", "babel-preset-env": "^1.7.0", + "chalk": "^2.4.1", "concurrently": "^3.5.1", "conventional-changelog-ember": "^2.0.0", "express": "^4.16.4", diff --git a/yarn.lock b/yarn.lock index 204a16d..fc4909d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,10 @@ # yarn lockfile v1 +"@artsy/detect-responsive-traits@^0.0.1": + version "0.0.1" + resolved "https://registry.yarnpkg.com/@artsy/detect-responsive-traits/-/detect-responsive-traits-0.0.1.tgz#3d83384bf3323fbd4679a2d73155ab85dc73e850" + "@babel/cli@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.0.0.tgz#108b395fd43fff6681d36fb41274df4d8ffeb12e" @@ -826,6 +830,13 @@ "@types/connect" "*" "@types/node" "*" +"@types/chalk@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@types/chalk/-/chalk-2.2.0.tgz#b7f6e446f4511029ee8e3f43075fb5b73fbaa0ba" + integrity sha512-1zzPV9FDe1I/WHhRkf9SNgqtRJWZqrBWgu7JGveuHmmyR9CnAPCie2N/x+iHrgnpYBIcCJWHBoMRv2TRWktsvw== + dependencies: + chalk "*" + "@types/connect@*": version "3.4.32" resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.32.tgz#aa0e9616b9435ccad02bc52b5b454ffc2c70ba28" @@ -2283,6 +2294,14 @@ caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" +chalk@*, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.3.0, chalk@^2.3.1, chalk@^2.3.2, chalk@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e" + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + chalk@^1.0.0, chalk@^1.1.3: version "1.1.3" resolved "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -2293,14 +2312,6 @@ chalk@^1.0.0, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@^2.0.0, chalk@^2.0.1, chalk@^2.3.0, chalk@^2.3.1, chalk@^2.3.2, chalk@^2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e" - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - chokidar@^2.0.0, chokidar@^2.0.2, chokidar@^2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.4.tgz#356ff4e2b0e8e43e322d18a372460bbcf3accd26" From cdc50572451f76c79f27b7f93901c1ec411b99c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eloy=20Dur=C3=A1n?= Date: Fri, 30 Nov 2018 21:11:01 +0100 Subject: [PATCH 3/3] [example] Show approach to not loading hidden images --- example/app.tsx | 53 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/example/app.tsx b/example/app.tsx index 948367d..de78f3c 100644 --- a/example/app.tsx +++ b/example/app.tsx @@ -34,6 +34,35 @@ const LargeStyle: CSSProperties = { backgroundColor: "red", } +// From https://www.smashingmagazine.com/2013/07/simple-responsive-images-with-css-background-images/ +const Img: React.SFC< + { src: string; aspectRatio: number } & React.HTMLProps +> = ({ src, aspectRatio, style, ...props }) => ( + + + +) + export const App: React.SFC = () => (
@@ -135,5 +164,29 @@ export const App: React.SFC = () => (
+
+

Example of not loading hidden images

+ + + + + + + + + +
)