Skip to content

Commit e52e745

Browse files
author
Adrian Bece
committed
Modification
1 parent f950670 commit e52e745

File tree

6 files changed

+151
-123
lines changed

6 files changed

+151
-123
lines changed

README.md

+22-15
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

+6-104
Original file line numberDiff line numberDiff line change
@@ -1,110 +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
11-
scope: string
5+
options: hookOptions
126
}
137

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

106-
if (allFontsLoaded) {
107-
setHasLoaded(true)
108-
}
109-
}
11+
return <>{children}</>
11012
}

consts/defaults.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ export const TIMEOUT_DEFAULT = 30000
44

55
export const MODE_DEFAULT = "async"
66

7-
export const SCOPE_DEFAULT = "body"
7+
export const SCOPE_DEFAULT = "body"

gatsby-browser.js

+13-3
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, SCOPE_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 = (
@@ -28,11 +33,16 @@ export const wrapRootElement = (
2833
const hasFontFiles = Boolean(fontFiles.length)
2934
const hasFontNames = Boolean(fontNames.length)
3035

31-
return (
36+
const children = (
3237
<>
3338
{hasFontNames && <AsyncFonts hrefs={fontFiles} />}
34-
{enableListener && hasFontFiles && <FontListener {...listenerProps} />}
3539
{element}
3640
</>
3741
)
42+
43+
if (!hasFontFiles || !enableListener) {
44+
return children
45+
}
46+
47+
return <FontListener options={listenerProps}>{children}</FontListener>
3848
}

hooks/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./useFontListener"

hooks/useFontListener.tsx

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import React, { 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)