├── .gitignore ├── .setup.js ├── .travis.yml ├── LICENSE ├── jest.e2e.json ├── jest.unit.json ├── package.json ├── readme.md ├── src ├── __snapshots__ │ ├── font-observer.spec.tsx.snap │ └── font-subscriber.spec.tsx.snap ├── font-observer.spec.tsx ├── font-observer.ts ├── font-subscriber.spec.tsx ├── font-subscriber.ts ├── index.ts ├── with-fonts.spec.tsx └── with-fonts.tsx ├── test ├── __fixtures__ │ ├── index.html │ └── index.tsx ├── e2e.spec.ts └── webpack.config.js ├── tsconfig.json ├── tslint.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | dist/ 4 | lib/ 5 | .vscode/ 6 | .DS_Store 7 | *.log 8 | test/__fixtures__/bundle.js -------------------------------------------------------------------------------- /.setup.js: -------------------------------------------------------------------------------- 1 | // Polyfill requestAnimationFrame before using React (and Enzyme) 2 | global.requestAnimationFrame = callback => global.setTimeout(callback, 0); 3 | 4 | // Setup Enzyme for React 16 5 | const Enzyme = require('enzyme'); 6 | const Adapter = require('enzyme-adapter-react-16'); 7 | Enzyme.configure({ adapter: new Adapter() }); 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 8 4 | - 10 5 | sudo: required 6 | addons: 7 | chrome: stable 8 | cache: 9 | directories: 10 | - node_modules 11 | before_install: 12 | - yarn global add greenkeeper-lockfile@1 13 | install: 14 | - yarn 15 | before_script: 16 | - greenkeeper-lockfile-update 17 | script: 18 | - yarn lint 19 | - yarn test 20 | - yarn e2e 21 | after_script: 22 | - greenkeeper-lockfile-upload 23 | after_success: 24 | - yarn coverage -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-present Sergey Bekrin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /jest.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts", "tsx"], 3 | "transform": { 4 | "^.+\\.(ts|tsx)$": "ts-jest" 5 | }, 6 | "testMatch": ["/test/**/*.spec.ts"], 7 | "globals": { 8 | "ts-jest": { 9 | "diagnostics": false 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /jest.unit.json: -------------------------------------------------------------------------------- 1 | { 2 | "setupFiles": ["./.setup.js"], 3 | "moduleFileExtensions": ["js", "json", "ts", "tsx"], 4 | "transform": { 5 | "^.+\\.(ts|tsx)$": "ts-jest" 6 | }, 7 | "testMatch": ["/src/**/*.spec.tsx"], 8 | "collectCoverageFrom": ["src/**/*", "!src/**/*.spec.*"], 9 | "globals": { 10 | "ts-jest": { 11 | "diagnostics": false 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-with-async-fonts", 3 | "version": "4.0.3", 4 | "description": "React HoC to handle async loaded fonts", 5 | "repository": "https://github.com/sbekrin/react-with-async-fonts", 6 | "main": "./lib/index.js", 7 | "module": "./dist/index.js", 8 | "scripts": { 9 | "prepublishOnly": "npm run lint && npm run test && npm run e2e && npm run build", 10 | "prebuild": "rimraf lib/ dist/", 11 | "build": "npm run build:cjs && npm run build:esm", 12 | "build:cjs": "tsc -d -t es5 -m commonjs --outDir ./lib", 13 | "build:esm": "tsc -d -t es6 -m es6 --outDir ./dist", 14 | "test": "jest --config jest.unit.json --coverage --no-cache", 15 | "pree2e": "npm run build && webpack --config test/webpack.config.js", 16 | "e2e": "jest --config jest.e2e.json", 17 | "coverage": "cat ./coverage/remapped/lcov.info | coveralls", 18 | "lint": "tslint src/**/*.{ts,tsx}" 19 | }, 20 | "files": [ 21 | "lib/", 22 | "dist/", 23 | "readme.md" 24 | ], 25 | "keywords": [ 26 | "react", 27 | "fonts", 28 | "async", 29 | "webfont", 30 | "fontloader", 31 | "react-fonts" 32 | ], 33 | "author": "Sergey Bekrin (https://bekrin.me)", 34 | "license": "MIT", 35 | "dependencies": { 36 | "fontfaceobserver": "^2.0.13", 37 | "hoist-non-react-statics": "^3.0.1", 38 | "invariant": "^2.2.4", 39 | "p-cancelable": "^0.5.0" 40 | }, 41 | "peerDependencies": { 42 | "react": ">=15", 43 | "react-dom": ">=15" 44 | }, 45 | "devDependencies": { 46 | "@types/jest": "^24.0.0", 47 | "@types/react": "^16.4.16", 48 | "coveralls": "^3.0.2", 49 | "doctoc": "^1.3.1", 50 | "enzyme": "^3.7.0", 51 | "enzyme-adapter-react-16": "^1.6.0", 52 | "express": "^4.16.3", 53 | "get-port": "^4.0.0", 54 | "husky": "^1.1.1", 55 | "jest": "^23.6.0", 56 | "lint-staged": "^8.0.0", 57 | "prettier": "^1.14.3", 58 | "prop-types": "^15.6.2", 59 | "puppeteer": "^1.9.0", 60 | "react": "^16.5.2", 61 | "react-dom": "^16.5.2", 62 | "react-test-renderer": "^16.5.2", 63 | "rimraf": "^2.6.2", 64 | "ts-jest": "^24.0.0", 65 | "ts-loader": "^5.2.1", 66 | "tslint": "^5.11.0", 67 | "typescript": "^3.1.1", 68 | "webpack": "^4.20.2", 69 | "webpack-cli": "^3.1.2" 70 | }, 71 | "husky": { 72 | "pre-commit": "lint-staged" 73 | }, 74 | "prettier": { 75 | "trailingComma": "all", 76 | "singleQuote": true, 77 | "proseWrap": "always" 78 | }, 79 | "lint-staged": { 80 | "*.ts": [ 81 | "prettier --write", 82 | "tslint --fix", 83 | "jest --findRelatedTests --passWithNoTests", 84 | "git add" 85 | ], 86 | "readme.md": [ 87 | "prettier --write", 88 | "doctoc --maxlevel 3 --notitle", 89 | "git add" 90 | ], 91 | "*.{json,md}": [ 92 | "prettier --write", 93 | "git add" 94 | ] 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # react-with-async-fonts 2 | 3 | [![npm Version](https://img.shields.io/npm/v/react-with-async-fonts.svg?maxAge=0)](https://www.npmjs.com/package/react-with-async-fonts) 4 | [![Build Status](https://img.shields.io/travis/sbekrin/react-with-async-fonts.svg?maxAge=0)](https://travis-ci.org/sbekrin/react-with-async-fonts) 5 | [![Coverage Status](https://img.shields.io/coveralls/sbekrin/react-with-async-fonts.svg?maxAge=0)](https://coveralls.io/github/sbekrin/react-with-async-fonts?branch=master) 6 | [![Greenkeeper badge](https://badges.greenkeeper.io/sbekrin/react-with-async-fonts.svg)](https://greenkeeper.io/) 7 | 8 | React module for working with async loaded custom web fonts, based on [`fontfaceobserver`](https://fontfaceobserver.com/). 9 | 10 | > Note: version 4.x introduces breaking changes with new API. It addresses bunch 11 | > of issues, including canceling promises, better performance, and TS typings. 12 | 13 | 14 | 15 | 16 | 17 | - [Quick Start](#quick-start) 18 | - [API](#api) 19 | - [`FontObserver` component](#fontobserver-component) 20 | - [`FontSubscriber` component](#fontsubscriber-component) 21 | - [`withFonts` HoC](#withfonts-hoc) 22 | - [`Font` type](#font-type) 23 | - [Examples](#examples) 24 | - [Basic with `FontSubscriber`](#basic-with-fontsubscriber) 25 | - [Basic with `withFonts`](#basic-with-withfonts) 26 | - [With `styled-components`](#with-styled-components) 27 | - [Nested `FontObserver`](#nested-fontobserver) 28 | - [Custom `fontfaceobserver` options](#custom-fontfaceobserver-options) 29 | - [License](#license) 30 | 31 | 32 | 33 | ## Quick Start 34 | 35 | 1. Install `react-with-async-fonts`: 36 | 37 | npm: 38 | 39 | ```bash 40 | npm install --save react-with-async-fonts 41 | ``` 42 | 43 | yarn: 44 | 45 | ```bash 46 | yarn add react-with-async-fonts 47 | ``` 48 | 49 | 2. Wrap your root component with `FontObserver`: 50 | 51 | Set prop with font name. You can access it later in `FontSubscriber` to check if 52 | it's ready. 53 | 54 | ```js 55 | import { FontObserver } from 'react-with-async-fonts'; 56 | import { render } from 'react-dom'; 57 | import App from './app'; 58 | 59 | render( 60 | 61 | 62 | , 63 | document.getElementById('root'), 64 | ); 65 | ``` 66 | 67 | 3. Wrap your target with `FontSubscriber` component: 68 | 69 | > Tip: you can also use [`withFonts` API](#withFonts) if you're really into 70 | > HoCs. 71 | 72 | Note that `FontSubscriber` uses children render prop. Provided function would be 73 | called with single argument which is an object with loaded font keys. 74 | 75 | ```js 76 | import { FontSubscriber } from 'react-with-async-fonts'; 77 | 78 | const Heading = ({ children }) => ( 79 | 80 | {fonts => ( 81 |

82 | {children} 83 |

84 | )} 85 |
86 | ); 87 | 88 | export default Heading; 89 | ``` 90 | 91 | ## API 92 | 93 | ### `FontObserver` component 94 | 95 | ```js 96 | import { FontObserver } from 'react-with-async-fonts'; 97 | ``` 98 | 99 | | Prop | Type | Description | 100 | | --------- | --------------- | ---------------------------------------------------- | 101 | | `text` | `string` | `fontfaceobserver`'s `.load` text options | 102 | | `timeout` | `number` | `fontfaceobserver`'s `.load` timeout options | 103 | | `[key]` | `Font \| string` | Font family string or a [`Font` object](#font-type). | 104 | 105 | ### `FontSubscriber` component 106 | 107 | ```js 108 | import { FontSubscriber } from 'react-with-async-fonts'; 109 | ``` 110 | 111 | | Prop | Type | Description | 112 | | ---------- | --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | 113 | | `children` | `(fonts: Object) => React.Element` | Children render function. Accepts object with loaded font. Once ready, it would contain object of [`Font` type](#font-type). | 114 | 115 | ### `withFonts` HoC 116 | 117 | ```js 118 | import { withFonts } from 'react-with-async-fonts'; 119 | ``` 120 | 121 | | Argument | Type | Description | 122 | | --------- | -------------------------- | --------------------------------------------------- | 123 | | component | `React.ComponentType` | Component to wrap with HoC. Injects `fonts` object. | 124 | 125 | ### `Font` type 126 | 127 | ```js 128 | type Font = { 129 | family: String, 130 | weight?: 131 | | 'normal' 132 | | 'bold' 133 | | 'bolder' 134 | | 'lighter' 135 | | '100' 136 | | '200' 137 | | '300' 138 | | '400' 139 | | '500' 140 | | '600' 141 | | '700' 142 | | '800' 143 | | '900', 144 | style?: 'normal' | 'italic' | 'oblique', 145 | stretch?: 146 | | 'normal' 147 | | 'ultra-condensed' 148 | | 'extra-condensed' 149 | | 'condensed' 150 | | 'semi-condensed' 151 | | 'semi-expanded' 152 | | 'expanded' 153 | | 'extra-expanded' 154 | | 'ultra-expanded', 155 | }; 156 | ``` 157 | 158 | ## Examples 159 | 160 | Heads up! Each example requires wrapping your app with 161 | [`FontObserver`](#fontobserver-component): 162 | 163 | ```js 164 | import React from 'react'; 165 | import { render } from 'react-dom'; 166 | import { FontObserver } from 'react-with-async-fonts'; 167 | import App from './app'; 168 | 169 | render( 170 | 171 | 172 | , 173 | document.getElementById('root'), 174 | ); 175 | ``` 176 | 177 | ### Basic with `FontSubscriber` 178 | 179 | ```js 180 | import React from 'react'; 181 | import { FontSubscriber } from 'react-with-async-fonts'; 182 | 183 | const Heading = ({ children }) => ( 184 | 185 | {fonts => ( 186 |

{children}

187 | )} 188 |
189 | ); 190 | 191 | export default Heading; 192 | ``` 193 | 194 | ### Basic with `withFonts` 195 | 196 | You can use `withFonts` HoC if you want to compose your component. Please note 197 | it uses same `FontSubscriber` under the hood. 198 | 199 | ```js 200 | import React from 'react'; 201 | import { withFonts } from 'react-with-async-fonts'; 202 | 203 | const Heading = ({ children, fonts }) => ( 204 |

{children}

205 | ); 206 | 207 | export default withFonts(Heading); 208 | ``` 209 | 210 | ### With `styled-components` 211 | 212 | Most elegant way of using it with `styled-components` is `withFonts` HoC. 213 | 214 | ```js 215 | import styled from 'styled-components'; 216 | import { withFonts } from 'react-with-async-fonts'; 217 | 218 | const Heading = styled.h2` 219 | font-weight: 300; 220 | font-family: ${props => 221 | props.fonts.montserrat 222 | ? '"Open Sans", sans-serif' 223 | : 'Helvetica, sans-serif'}; 224 | `; 225 | 226 | export default withFonts(Heading); 227 | ``` 228 | 229 | ### Nested `FontObserver` 230 | 231 | You can nest `FontObserver` to merge fonts. Children instances overrides parent 232 | if font with same code was defined. 233 | 234 | ```js 235 | import { FontObserver, FontSubscriber } from 'react-with-async-fonts'; 236 | 237 | const Article = ({ title, children }) => ( 238 | 239 | 240 | 241 | {fonts => ( 242 |
243 |

{title}

244 |

{children}

245 |
246 | )} 247 |
248 |
249 |
250 | ); 251 | 252 | export default Article; 253 | ``` 254 | 255 | ### Custom `fontfaceobserver` options 256 | 257 | You can provide `text` and `timeout` options for 258 | [`fontfaceobserver`'s .load](https://github.com/bramstein/fontfaceobserver#how-to-use) 259 | method with same props. 260 | 261 | ```js 262 | import { FontObserver, FontSubscriber } from 'react-with-async-fonts'; 263 | 264 | const Heading = ({ children }) => ( 265 | 266 | 267 | {fonts =>

{children}

} 268 |
269 |
270 | ); 271 | 272 | export default Heading; 273 | ``` 274 | 275 | ## License 276 | 277 | MIT © [Sergey Bekrin](http://bekrin.me) 278 | -------------------------------------------------------------------------------- /src/__snapshots__/font-observer.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` validates font props 1`] = `"Expected font prop to be a string or object, received number instead"`; 4 | 5 | exports[` validates font props 2`] = `"Expected font 'family' prop to be a non-empty string, received array instead"`; 6 | 7 | exports[` validates font props 3`] = `"Expected font 'family' prop to be a non-empty string, received null instead"`; 8 | -------------------------------------------------------------------------------- /src/__snapshots__/font-subscriber.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` only accepts function child 1`] = ` 4 | "Warning: Failed prop type: Invalid prop \`children\` of type \`string\` supplied to \`FontSubscriber\`, expected \`function\`. 5 | in FontSubscriber" 6 | `; 7 | -------------------------------------------------------------------------------- /src/font-observer.spec.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as T from 'prop-types'; 3 | import * as FontFaceObserver from 'fontfaceobserver'; 4 | import { shallow, mount } from 'enzyme'; 5 | import { FontObserver } from './index'; 6 | 7 | describe('', () => { 8 | let ffoLoadSpy: jest.SpyInstance; 9 | 10 | beforeEach(() => { 11 | jest.useFakeTimers(); 12 | ffoLoadSpy = jest.spyOn(FontFaceObserver.prototype, 'load'); 13 | ffoLoadSpy.mockImplementation(() => Promise.resolve()); 14 | }); 15 | 16 | afterEach(() => { 17 | ffoLoadSpy.mockReset(); 18 | jest.useRealTimers(); 19 | }); 20 | 21 | it('validates font props', () => { 22 | expect(() => 23 | shallow(), 24 | ).toThrowErrorMatchingSnapshot(); 25 | expect(() => 26 | shallow(), 27 | ).toThrowErrorMatchingSnapshot(); 28 | expect(() => 29 | shallow(), 30 | ).toThrowErrorMatchingSnapshot(); 31 | }); 32 | 33 | it('reflects font loading on state', async () => { 34 | const wrapper = shallow(); 35 | // not sure how to flush promises in more elegant way with Jest 36 | await Promise.resolve(); 37 | await Promise.resolve(); 38 | expect(wrapper.state().openSans).toEqual({ family: 'Open Sans' }); 39 | }); 40 | 41 | it('passes text and timeout options to FFO', () => { 42 | mount(); 43 | jest.runOnlyPendingTimers(); 44 | expect(ffoLoadSpy).toHaveBeenCalledWith('foo', 5000); 45 | }); 46 | 47 | it('cancels FFO promises before it unmounts', () => { 48 | ffoLoadSpy.mockImplementation(() => new Promise(r => setTimeout(r, 5000))); 49 | mount().unmount(); 50 | jest.runOnlyPendingTimers(); 51 | }); 52 | 53 | it('merges with parent context', async () => { 54 | const Test = jest.fn().mockReturnValue(); 55 | Test.contextTypes = { __fonts: T.object }; 56 | mount( 57 | 58 | 59 | 60 | 61 | 62 | 63 | , 64 | ); 65 | await Promise.resolve(); 66 | await Promise.resolve(); 67 | expect(Test).toHaveBeenCalledWith(expect.any(Object), { 68 | __fonts: { 69 | openSans: expect.any(Object), 70 | roboto: expect.any(Object), 71 | ptSans: expect.any(Object), 72 | }, 73 | }); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /src/font-observer.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as T from 'prop-types'; 3 | import * as FontFaceObserver from 'fontfaceobserver'; 4 | import * as Cancelable from 'p-cancelable'; 5 | import * as invariant from 'invariant'; 6 | 7 | export interface Font { 8 | family: string; 9 | weight?: 10 | | 'normal' 11 | | 'bold' 12 | | 'bolder' 13 | | 'lighter' 14 | | '100' 15 | | '200' 16 | | '300' 17 | | '400' 18 | | '500' 19 | | '600' 20 | | '700' 21 | | '800' 22 | | '900'; 23 | style?: 'normal' | 'italic' | 'oblique'; 24 | stretch?: 25 | | 'normal' 26 | | 'ultra-condensed' 27 | | 'extra-condensed' 28 | | 'condensed' 29 | | 'semi-condensed' 30 | | 'semi-expanded' 31 | | 'expanded' 32 | | 'extra-expanded' 33 | | 'ultra-expanded'; 34 | } 35 | 36 | /** Checks if value is an actual object */ 37 | const isObject = (value: any): value is object => { 38 | return value !== null && !Array.isArray(value) && typeof value === 'object'; 39 | }; 40 | 41 | /** Checks if value is a string */ 42 | const isString = (value: any): value is string => { 43 | return typeof value === 'string'; 44 | }; 45 | 46 | /** Returns actual type of value */ 47 | const getType = value => { 48 | if (value === null) { 49 | return 'null'; 50 | } else if (Array.isArray(value)) { 51 | return 'array'; 52 | } 53 | return typeof value; 54 | }; 55 | 56 | export interface ObserverState { 57 | [key: string]: Font; 58 | } 59 | 60 | export type ObserverProps = { 61 | children: Array>; 62 | text?: string; 63 | timeout?: number; 64 | } & { 65 | [key: string]: Font | string; 66 | }; 67 | 68 | export interface ObserverContext { 69 | __fonts: ObserverState; 70 | } 71 | 72 | class FontObserver extends React.Component { 73 | static propTypes = { 74 | children: T.node, 75 | text: T.string, 76 | timeout: T.number, 77 | }; 78 | 79 | static defaultProps = { 80 | children: null, 81 | text: null, 82 | timeout: 3000, 83 | }; 84 | 85 | static childContextTypes = { 86 | __fonts: T.object, 87 | }; 88 | 89 | static contextTypes = { 90 | __fonts: T.object, 91 | }; 92 | 93 | promises: Array> = []; 94 | 95 | state = {}; 96 | 97 | getChildContext() { 98 | // Merge fonts contexts 99 | const passedDownFonts = this.context.__fonts; 100 | const currentFonts = this.state; 101 | return { __fonts: { ...passedDownFonts, ...currentFonts } }; 102 | } 103 | 104 | componentDidMount() { 105 | const { children, text, timeout, ...props } = this.props; 106 | // Keep promises to cancel them once component unmounts 107 | this.promises = Object.keys(props).map(prop => { 108 | // Validate stuff 109 | const origValue = props[prop]; 110 | invariant( 111 | isObject(origValue) || isString(origValue), 112 | `Expected font prop to be a string or object, received ${getType( 113 | origValue, 114 | )} instead`, 115 | ); 116 | const value = isObject(origValue) ? origValue : { family: origValue }; 117 | invariant( 118 | isString(value.family) && value.family.length > 0, 119 | `Expected font 'family' prop to be a non-empty string, received ${getType( 120 | value.family, 121 | )} instead`, 122 | ); 123 | const { family, ...rest } = value; 124 | // Allow cancelling FFO promises 125 | const ffo = new Cancelable((resolve, reject) => 126 | new FontFaceObserver(family, rest) 127 | .load(text, timeout) 128 | .then(resolve, reject), 129 | ); 130 | // Update state once resolved 131 | ffo 132 | .then(() => this.setState(prev => ({ ...prev, [prop]: value }))) 133 | .catch(reason => !ffo.canceled && Promise.reject(reason)); 134 | 135 | return ffo; 136 | }); 137 | } 138 | 139 | componentWillUnmount() { 140 | // Mark promises as canceled to avoid setState calls when unmounted 141 | this.promises.forEach(promise => promise.cancel()); 142 | } 143 | 144 | render() { 145 | return this.props.children; 146 | } 147 | } 148 | 149 | export default FontObserver; 150 | -------------------------------------------------------------------------------- /src/font-subscriber.spec.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { FontSubscriber } from './index'; 4 | 5 | describe('', () => { 6 | it('only accepts function child', () => { 7 | // Make console.error to throw to catch it with .toThrow 8 | global.console.error = jest.fn(reason => { 9 | throw new Error(reason); 10 | }); 11 | expect(() => ( 12 | Hello 13 | )).toThrowErrorMatchingSnapshot(); 14 | // Reset console hack 15 | global.console.error.mockReset(); 16 | }); 17 | 18 | it('calls function child with fonts context', () => { 19 | const funcChild = jest.fn(); 20 | shallow({funcChild}, { 21 | context: { __fonts: { openSans: { family: 'Open Sans' } } }, 22 | }); 23 | expect(funcChild).toHaveBeenCalledWith({ 24 | openSans: { family: 'Open Sans' }, 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/font-subscriber.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as T from 'prop-types'; 3 | import { ObserverContext, ObserverState } from './font-observer'; 4 | 5 | export interface SubscriberProps { 6 | children: (fonts: ObserverState) => React.ReactElement; 7 | } 8 | 9 | class FontSubscriber extends React.Component { 10 | static contextTypes = { 11 | __fonts: T.object, 12 | }; 13 | 14 | static propTypes = { 15 | children: T.func.isRequired, 16 | }; 17 | 18 | context: ObserverContext; 19 | 20 | render() { 21 | return this.props.children(this.context.__fonts); 22 | } 23 | } 24 | 25 | export default FontSubscriber; 26 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import FontObserver from './font-observer'; 2 | import FontSubscriber from './font-subscriber'; 3 | import withFonts from './with-fonts'; 4 | 5 | export { FontObserver, FontSubscriber, withFonts }; 6 | -------------------------------------------------------------------------------- /src/with-fonts.spec.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as T from 'prop-types'; 3 | import { mount } from 'enzyme'; 4 | import { withFonts } from './index'; 5 | 6 | describe('withFonts()', () => { 7 | it('injects fonts prop', async () => { 8 | const Test = jest.fn().mockReturnValue(
); 9 | const TestWithFonts = withFonts(Test); 10 | mount(, { 11 | context: { __fonts: { openSans: { family: 'Open Sans' } } }, 12 | childContextTypes: { __fonts: T.object }, 13 | }); 14 | expect(Test).toHaveBeenCalledWith( 15 | { fonts: { openSans: { family: 'Open Sans' } } }, 16 | expect.any(Object), 17 | ); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/with-fonts.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as hoistStatics from 'hoist-non-react-statics'; 3 | import FontSubscriber from './font-subscriber'; 4 | 5 | const h = React.createElement; 6 | 7 | function withFonts(Target) { 8 | const Composed = props => 9 | h(FontSubscriber, { 10 | children: fonts => h(Target, { ...props, fonts }), 11 | }); 12 | return hoistStatics(Composed, Target); 13 | } 14 | 15 | export default withFonts; 16 | -------------------------------------------------------------------------------- /test/__fixtures__/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /test/__fixtures__/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import { FontObserver, FontSubscriber } from '../../'; 4 | 5 | // See https://github.com/Microsoft/TypeScript/issues/11152 6 | const props: any = { roboto900: { family: 'Roboto', weight: '900' }}; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | {fonts => ( 12 |

13 | Hello 14 |

15 | )} 16 |
17 |
, 18 | document.getElementById('root'), 19 | ); 20 | -------------------------------------------------------------------------------- /test/e2e.spec.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import * as puppeteer from 'puppeteer'; 3 | import * as express from 'express'; 4 | import * as getPort from 'get-port'; 5 | import { Server } from 'http'; 6 | 7 | describe('Integration', () => { 8 | let server: Server; 9 | let browser: puppeteer.Browser; 10 | let page: puppeteer.Page; 11 | 12 | beforeAll(async () => { 13 | const port = await getPort(); 14 | const app = express(); 15 | app.use(express.static(join(__dirname, '__fixtures__'))); 16 | server = app.listen(port); 17 | browser = await puppeteer.launch(); 18 | page = await browser.newPage(); 19 | page.goto(`http://localhost:${port}`); 20 | }); 21 | 22 | afterAll(async () => { 23 | await browser.close(); 24 | await server.close(); 25 | }); 26 | 27 | it('sets class name once font is ready', async () => { 28 | await page.waitForSelector('.system-font'); 29 | await page.waitForSelector('.roboto900-font'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: 'production', 5 | entry: './index.tsx', 6 | context: path.resolve(__dirname, '__fixtures__'), 7 | output: { 8 | path: path.resolve(__dirname, '__fixtures__'), 9 | filename: 'bundle.js', 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.tsx?$/, 15 | exclude: /node_modules/, 16 | loader: 'ts-loader', 17 | }, 18 | ], 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "jsx": "react", 5 | "lib": ["dom", "es2017"], 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["src"], 9 | "exclude": ["**/*.spec.tsx"] 10 | } 11 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended"], 3 | "rules": { 4 | "member-access": false, 5 | "quotemark": [true, "single", "jsx-double"], 6 | "interface-name": [true, "never-prefix"], 7 | "ordered-imports": false, 8 | "object-literal-sort-keys": false, 9 | "arrow-parens": false, 10 | "no-reference": false 11 | } 12 | } 13 | --------------------------------------------------------------------------------