Skip to content

Commit ba8cf83

Browse files
authored
Merge pull request #14 from fcisio/class-to-attributes
Change loaded font behavior
2 parents 6ecdf28 + d5515f0 commit ba8cf83

File tree

6 files changed

+154
-119
lines changed

6 files changed

+154
-119
lines changed

README.md

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,6 @@ Add the following snippet to `gatsby-config.js` plugins array.
5656
/* Preconnect URL-s. This example is for Google Fonts */
5757
preconnect: ["https://fonts.gstatic.com"],
5858

59-
/* Font listener interval (in ms). Default is 300ms. Recommended: >=300ms */
60-
interval: 300,
61-
62-
/* Font listener timeout value (in ms). Default is 30s (30000ms). Listener will no longer check for loaded fonts after timeout, fonts will still be loaded and displayed, but without handling FOUT. */
63-
timeout: 30000,
64-
6559
/* Self-hosted fonts config. Add font files and font CSS files to "static" folder */
6660
custom: [
6761
{
@@ -96,10 +90,15 @@ Add the following snippet to `gatsby-config.js` plugins array.
9690
</thead>
9791
<tbody>
9892
<tr>
99-
</tr>
10093
<td>mode</td>
101-
<td>Can be set to <code>"async"</code> (default) or <code>"render-blocking"</code>. In <code>async</code> mode, fonts are loaded in optimal way, but FOUT is visible. In <code>render-blocking</code> mode FOUT will happen in rare cases, but the font files will become render-blocking.</td>
94+
<td>Can be set to <code>async</code> (default) or <code>render-blocking</code>. In <code>async</code> mode, fonts are loaded in optimal way, but FOUT is visible. In <code>render-blocking</code> mode FOUT will happen in rare cases, but the font files will become render-blocking.</td>
10295
<td>async</td>
96+
</tr>
97+
<tr>
98+
<td>scope</td>
99+
<td>Can be set to <code>body</code> (default) or <code>html</code>. Sets the target element for HTML classnames to be applied to.</td>
100+
<td>body</td>
101+
</tr>
103102
<tr>
104103
<td>enableListener</td>
105104
<td>Works in <code>async</code> mode. Enable font loading listener to handle Flash Of Unstyled Text. If enabled, CSS classes will be applied to HTML once each font has finished loading.</td>
@@ -117,23 +116,23 @@ Add the following snippet to `gatsby-config.js` plugins array.
117116
</tr>
118117
<tr>
119118
<td>custom</td>
120-
<td>Self-hosted fonts config. Add font files and font CSS files to "static" folder. Array of <code>{name: "Font name", file: "https://url-to-font-css.path"}</code> objects.</td>
121-
<td>[]</td>
119+
<td>Self-hosted fonts config. Add font files and font CSS files to <code>static</code> folder. Array of <code>{name: "Font name", file: "https://url-to-font-css.path"}</code> objects.</td>
120+
<td><code>[]</code></td>
122121
</tr>
123122
<tr>
124123
<td>web</td>
125124
<td>Web fonts config. File link should point to font CSS file. Array of <code>{name: "Font name", file: "https://url-to-font-css.path"}</code> objects.</td>
126-
<td>[]</td>
125+
<td><code>[]</code></td>
127126
</tr>
128127
<tr>
129128
<td>preconnect</td>
130129
<td>URLs used for preconnect meta. Base URL where <strong>font files</strong> are hosted.</td>
131-
<td>[]</td>
130+
<td><code>[]</code></td>
132131
</tr>
133132
<tr>
134133
<td>preload</td>
135-
<td>Additional URLs used for preload meta. Preload for URLs provided under `file` attribute of `custom` and `web` fonts are automatically generated.</td>
136-
<td>[]</td>
134+
<td>Additional URLs used for preload meta. Preload for URLs provided under <code>file</code> attribute of <code>custom</code> and <code>web</code> fonts are automatically generated.</td>
135+
<td><code>[]</code></td>
137136
</tr>
138137
<tbody>
139138
</table>
@@ -181,7 +180,15 @@ Feel free to [report issues](https://github.com/codeAdrian/gatsby-omni-font-load
181180

182181
Contributions are welcome and appreciated!
183182

184-
## Thank you for the support
183+
## Code contributors
184+
185+
Thank you for your contribution!
186+
187+
[Henrik](https://github.com/henrikdahl)[Lennart](https://github.com/LekoArts)[Francis Champagne](https://github.com/fcisio)
188+
189+
## Sponsors
190+
191+
Thank you for your support!
185192

186193
[Roboto Studio](https://roboto.studio/)[Your Name Here](https://www.buymeacoffee.com/ubnZ8GgDJ/e/11337)
187194

components/FontListener.tsx

Lines changed: 6 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,106 +1,12 @@
1-
import React, { useEffect, useMemo, useRef, useState } from "react"
2-
import { Helmet } from "react-helmet"
3-
import { kebabCase } from "../utils"
4-
5-
declare var document: { fonts: any }
1+
import React from "react"
2+
import { hookOptions, useFontListener } from "../hooks"
63

74
interface Props {
8-
fontNames: string[]
9-
interval: number
10-
timeout: number
5+
options: hookOptions
116
}
127

13-
export const FontListener: React.FC<Props> = ({
14-
fontNames,
15-
interval,
16-
timeout,
17-
}) => {
18-
const [hasLoaded, setHasLoaded] = useState<Boolean>(false)
19-
const [loadedFonts, setLoadedFonts] = useState<string[]>([])
20-
const [intervalId, setIntervalId] = useState<number>(-1)
21-
const attempts = useRef<number>(Math.floor(timeout / interval))
22-
23-
const pendingFonts = useMemo(
24-
() => fontNames.filter(fontName => !loadedFonts.includes(fontName)),
25-
[loadedFonts, fontNames]
26-
)
27-
28-
const loadedClassname = useMemo(getLoadedFontClassNames, [loadedFonts])
29-
30-
const apiAvailable = "fonts" in document
31-
32-
useEffect(() => {
33-
if (!apiAvailable) {
34-
handleApiError("Font loading API not available")
35-
return
36-
}
37-
38-
if (apiAvailable && !hasLoaded && intervalId < 0) {
39-
const id = window.setInterval(isFontLoaded, interval)
40-
setIntervalId(id)
41-
}
42-
}, [hasLoaded, intervalId, apiAvailable])
43-
44-
useEffect(() => {
45-
if (hasLoaded && intervalId > 0) {
46-
clearInterval(intervalId)
47-
}
48-
}, [hasLoaded, intervalId])
49-
50-
return (
51-
<Helmet>
52-
<body className={loadedClassname} />
53-
</Helmet>
54-
)
55-
56-
function getLoadedFontClassNames() {
57-
return Boolean(loadedFonts.length)
58-
? loadedFonts
59-
.map(fontName => `wf-${kebabCase(fontName)}--loaded`)
60-
.join(" ")
61-
: ""
62-
}
63-
64-
function errorFallback() {
65-
setHasLoaded(true)
66-
setLoadedFonts(fontNames)
67-
}
68-
69-
function handleApiError(error) {
70-
console.info(`document.fonts API error: ${error}`)
71-
console.info(`Replacing fonts instantly. FOUT handling failed due.`)
72-
errorFallback()
73-
}
74-
75-
function isFontLoaded() {
76-
const loaded = []
77-
attempts.current = attempts.current - 1
78-
79-
if (attempts.current < 0) {
80-
handleApiError("Interval timeout reached, maybe due to slow connection.")
81-
}
82-
83-
const fontsLoading = pendingFonts.map(fontName => {
84-
let hasLoaded = false
85-
try {
86-
hasLoaded = document.fonts.check(`12px '${fontName}'`)
87-
} catch (error) {
88-
handleApiError(error)
89-
return
90-
}
91-
92-
if (hasLoaded) loaded.push(fontName)
93-
return hasLoaded
94-
})
95-
96-
const allFontsLoaded = fontsLoading.every(font => font)
97-
98-
if (Boolean(loaded.length)) {
99-
setLoadedFonts(loaded)
100-
}
8+
export const FontListener: React.FC<Props> = ({ children, options }) => {
9+
useFontListener(options)
10110

102-
if (allFontsLoaded) {
103-
setHasLoaded(true)
104-
}
105-
}
11+
return <>{children}</>
10612
}

consts/defaults.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@ export const INTERVAL_DEFAULT = 300
33
export const TIMEOUT_DEFAULT = 30000
44

55
export const MODE_DEFAULT = "async"
6+
7+
export const SCOPE_DEFAULT = "body"

gatsby-browser.js

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import React from "react"
22
import { AsyncFonts, FontListener } from "./components"
3-
import { INTERVAL_DEFAULT, MODE_DEFAULT, TIMEOUT_DEFAULT } from "./consts"
3+
import {
4+
INTERVAL_DEFAULT,
5+
MODE_DEFAULT,
6+
TIMEOUT_DEFAULT,
7+
SCOPE_DEFAULT,
8+
} from "./consts"
49
import { getFontFiles, getFontNames } from "./utils"
510

611
export const wrapRootElement = (
@@ -11,6 +16,7 @@ export const wrapRootElement = (
1116
enableListener,
1217
interval = INTERVAL_DEFAULT,
1318
timeout = TIMEOUT_DEFAULT,
19+
scope = SCOPE_DEFAULT,
1420
mode = MODE_DEFAULT,
1521
}
1622
) => {
@@ -22,16 +28,21 @@ export const wrapRootElement = (
2228
const fontFiles = getFontFiles(allFonts)
2329
const fontNames = getFontNames(allFonts)
2430

25-
const listenerProps = { fontNames, interval, timeout }
31+
const listenerProps = { fontNames, interval, timeout, scope }
2632

2733
const hasFontFiles = Boolean(fontFiles.length)
2834
const hasFontNames = Boolean(fontNames.length)
2935

30-
return (
36+
const children = (
3137
<>
3238
{hasFontNames && <AsyncFonts hrefs={fontFiles} />}
33-
{enableListener && hasFontFiles && <FontListener {...listenerProps} />}
3439
{element}
3540
</>
3641
)
42+
43+
if (!hasFontFiles || !enableListener) {
44+
return children
45+
}
46+
47+
return <FontListener options={listenerProps}>{children}</FontListener>
3748
}

hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./useFontListener"

hooks/useFontListener.tsx

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { useEffect, useMemo, useRef, useState } from "react"
2+
import { kebabCase } from "../utils"
3+
4+
declare var document: { fonts: any }
5+
6+
export type hookOptions = {
7+
fontNames: string[]
8+
interval: number
9+
timeout: number
10+
scope: string
11+
}
12+
13+
type fontListenerHook = (options: hookOptions) => void
14+
15+
export const useFontListener: fontListenerHook = ({
16+
fontNames,
17+
interval,
18+
timeout,
19+
scope,
20+
}) => {
21+
const [hasLoaded, setHasLoaded] = useState<Boolean>(false)
22+
const [loadedFonts, setLoadedFonts] = useState<string[]>([])
23+
const [intervalId, setIntervalId] = useState<number>(-1)
24+
const attempts = useRef<number>(Math.floor(timeout / interval))
25+
26+
const hasFonts = fontNames && Boolean(fontNames.length)
27+
28+
const pendingFonts = useMemo(
29+
() => fontNames.filter(fontName => !loadedFonts.includes(fontName)),
30+
[loadedFonts, fontNames]
31+
)
32+
const targetElement = useMemo(
33+
() => (scope === "html" ? "documentElement" : "body"),
34+
[scope]
35+
)
36+
37+
const apiAvailable = "fonts" in document
38+
39+
useEffect(() => {
40+
if (!apiAvailable) {
41+
handleApiError("Font loading API not available")
42+
return
43+
}
44+
45+
if (hasFonts && apiAvailable && !hasLoaded && intervalId < 0) {
46+
const id = window.setInterval(isFontLoaded, interval)
47+
setIntervalId(id)
48+
}
49+
}, [hasFonts, hasLoaded, intervalId, apiAvailable])
50+
51+
useEffect(() => {
52+
if (hasLoaded && intervalId > 0) {
53+
clearInterval(intervalId)
54+
}
55+
}, [hasLoaded, intervalId])
56+
57+
function errorFallback() {
58+
setHasLoaded(true)
59+
setLoadedFonts(fontNames)
60+
fontNames.forEach(addClassName)
61+
}
62+
63+
function handleApiError(error) {
64+
console.info(`document.fonts API error: ${error}`)
65+
console.info(`Replacing fonts instantly. FOUT handling failed.`)
66+
errorFallback()
67+
}
68+
69+
function addClassName(fontName) {
70+
document[targetElement].classList.add(`wf-${kebabCase(fontName)}--loaded`)
71+
}
72+
73+
function isFontLoaded() {
74+
const loaded = []
75+
attempts.current = attempts.current - 1
76+
77+
if (attempts.current < 0) {
78+
handleApiError("Interval timeout reached, maybe due to slow connection.")
79+
}
80+
81+
const fontsLoading = pendingFonts.map(fontName => {
82+
let hasLoaded = false
83+
try {
84+
hasLoaded = document.fonts.check(`12px '${fontName}'`)
85+
} catch (error) {
86+
handleApiError(error)
87+
return
88+
}
89+
90+
if (hasLoaded) {
91+
addClassName(fontName)
92+
loaded.push(fontName)
93+
}
94+
95+
return hasLoaded
96+
})
97+
98+
const allFontsLoaded = fontsLoading.every(font => font)
99+
100+
if (Boolean(loaded.length)) {
101+
setLoadedFonts(loaded)
102+
}
103+
104+
if (allFontsLoaded) {
105+
setHasLoaded(true)
106+
}
107+
}
108+
}

0 commit comments

Comments
 (0)