diff --git a/README.md b/README.md index 5f73e1b..2e7bc3d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@
Omni font loader logo

-

Gatsby Omni Font Loader

+

Gatsby Omni Font Loader v2

- Simple way to add webfonts or custom fonts to Gatsby project @@ -106,12 +106,12 @@ Add the following snippet to `gatsby-config.js` plugins array. false - interval + interval (V1 ONLY) Works if enableListener is true. Font listener interval (in ms). Default is 300ms. Recommended: >=300ms. 300 - timeout + timeout (V1 ONLY) Works if enableListener is true. 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. 30000 @@ -164,7 +164,7 @@ To avoid this, we can use CSS to style the fallback font to closely match the fo When `enableListener: true` is set in plugin config in `gatsby-config.js`, HTML classes are being added to `` element as the fonts are being loaded. -HTML class name format will be in the following format `wf-[font-family-name]--loaded`. +HTML class name format will be in the following format `wf-[font-family-name]`. When all fonts are loaded `wf-all` is applied. You can use the [Font Style Matcher](https://meowni.ca/font-style-matcher/) to adjust the perfect fallback font and fallback CSS config. @@ -172,12 +172,16 @@ Here is the example of how `body` element will look like after all fonts are bei ```html ``` FOUT example +## V2 breaking changes +* Removed `interval` and `timeout` options +* Changed class name format to a more generic `wf-[font-family-name]` to avoid mixing naming conventions + ## Issues and Contributions Feel free to [report issues](https://github.com/codeAdrian/gatsby-omni-font-loader/issues) you find and feel free to contribute to the project by creating Pull Requests. diff --git a/components/FontListener.tsx b/components/FontListener.tsx deleted file mode 100644 index f2c6b27..0000000 --- a/components/FontListener.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from "react" -import { hookOptions, useFontListener } from "../hooks" - -interface Props { - options: hookOptions -} - -export const FontListener: React.FC = ({ children, options }) => { - useFontListener(options) - - return <>{children} -} diff --git a/components/index.ts b/components/index.ts index 3ab98aa..9594226 100644 --- a/components/index.ts +++ b/components/index.ts @@ -1,2 +1 @@ -export * from "./AsyncFonts" -export * from "./FontListener" +export * from "./AsyncFonts"; diff --git a/consts/defaults.ts b/consts/defaults.ts index 30296b5..da455bd 100644 --- a/consts/defaults.ts +++ b/consts/defaults.ts @@ -1,7 +1,2 @@ -export const INTERVAL_DEFAULT = 300 - -export const TIMEOUT_DEFAULT = 30000 - -export const MODE_DEFAULT = "async" - -export const SCOPE_DEFAULT = "body" +export const MODE_DEFAULT = "async"; +export const SCOPE_DEFAULT = "body"; diff --git a/gatsby-browser.js b/gatsby-browser.js index dff81f0..4ba46ad 100644 --- a/gatsby-browser.js +++ b/gatsby-browser.js @@ -1,48 +1,41 @@ -import React from "react" -import { AsyncFonts, FontListener } from "./components" -import { - INTERVAL_DEFAULT, - MODE_DEFAULT, - TIMEOUT_DEFAULT, - SCOPE_DEFAULT, -} from "./consts" -import { getFontFiles, getFontNames } from "./utils" +import React from "react"; +import { AsyncFonts } from "./components"; +import { MODE_DEFAULT, SCOPE_DEFAULT } from "./consts"; +import { getFontFiles, getFontNames } from "./utils"; +import { fontListener } from "./utils/fontListener"; + +export const onClientEntry = ( + _, + { custom = [], web = [], enableListener = false, scope = SCOPE_DEFAULT } +) => { + if (!enableListener) { + return; + } + + const allFonts = [...custom, ...web]; + const fontNames = getFontNames(allFonts); + const listenerProps = { fontNames, scope }; + + fontListener(listenerProps); +}; export const wrapRootElement = ( { element }, - { - custom = [], - web = [], - enableListener, - interval = INTERVAL_DEFAULT, - timeout = TIMEOUT_DEFAULT, - scope = SCOPE_DEFAULT, - mode = MODE_DEFAULT, - } + { custom = [], web = [], mode = MODE_DEFAULT } ) => { if (mode !== "async") { - return element + return element; } - const allFonts = [...custom, ...web] - const fontFiles = getFontFiles(allFonts) - const fontNames = getFontNames(allFonts) + const allFonts = [...custom, ...web]; + const fontFiles = getFontFiles(allFonts); + const fontNames = getFontNames(allFonts); + const hasFontNames = Boolean(fontNames.length); - const listenerProps = { fontNames, interval, timeout, scope } - - const hasFontFiles = Boolean(fontFiles.length) - const hasFontNames = Boolean(fontNames.length) - - const children = ( + return ( <> {hasFontNames && } {element} - ) - - if (!hasFontFiles || !enableListener) { - return children - } - - return {children} -} + ); +}; diff --git a/gatsby-ssr.js b/gatsby-ssr.js index a4531fa..cef1b2a 100644 --- a/gatsby-ssr.js +++ b/gatsby-ssr.js @@ -1,34 +1,21 @@ -import { MODE_DEFAULT } from "./consts" -import { getFontConfig, getTestFonts } from "./generators" -import { getFontFiles, getFontNames } from "./utils" +import { MODE_DEFAULT } from "./consts"; +import { getFontConfig } from "./generators"; +import { getFontFiles } from "./utils"; export const onRenderBody = ( - { setHeadComponents, setPostBodyComponents }, - { - enableListener, - preconnect = [], - preload = [], - web = [], - custom = [], - mode = MODE_DEFAULT, - } + { setHeadComponents }, + { preconnect = [], preload = [], web = [], custom = [], mode = MODE_DEFAULT } ) => { - const allFonts = [...web, ...custom] - const allPreloads = preload.concat(getFontFiles(allFonts)) - const fontNames = getFontNames(allFonts) + const allFonts = [...web, ...custom]; + const allPreloads = preload.concat(getFontFiles(allFonts)); const preloadConfig = getFontConfig( preconnect, allPreloads, mode === "async" ? [] : allFonts - ) - - if (enableListener && Boolean(allFonts.length) && mode === "async") { - const testFontConfig = getTestFonts(fontNames) - setPostBodyComponents(testFontConfig) - } + ); if (preloadConfig && Boolean(preloadConfig.length)) { - setHeadComponents(preloadConfig) + setHeadComponents(preloadConfig); } -} +}; diff --git a/generators/getTestFonts.tsx b/generators/getTestFonts.tsx deleted file mode 100644 index 7c9eba0..0000000 --- a/generators/getTestFonts.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from "react" - -export const getTestFonts = (fontNames: string[]) => { - const fontConfig = [] - - const hiddenStyles: React.CSSProperties = { - position: "absolute", - overflow: "hidden", - clip: "rect(0 0 0 0)", - height: "1px", - width: "1px", - margin: "-1px", - padding: "0", - border: "0", - } - - fontNames.forEach(fontName => { - fontConfig.push( - - ) - }) - - return ( - - {fontConfig} - - ) -} diff --git a/generators/index.ts b/generators/index.ts index 2cd9208..e52d557 100644 --- a/generators/index.ts +++ b/generators/index.ts @@ -1,2 +1 @@ -export * from "./getFontConfig" -export * from "./getTestFonts" +export * from "./getFontConfig"; diff --git a/hooks/index.ts b/hooks/index.ts deleted file mode 100644 index 0e91a1a..0000000 --- a/hooks/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./useFontListener" diff --git a/hooks/useFontListener.tsx b/hooks/useFontListener.tsx deleted file mode 100644 index 5e2d1a4..0000000 --- a/hooks/useFontListener.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { useEffect, useMemo, useRef, useState } from "react" -import { kebabCase } from "../utils" - -declare var document: { fonts: any } - -export type hookOptions = { - fontNames: string[] - interval: number - timeout: number - scope: string -} - -type fontListenerHook = (options: hookOptions) => void - -export const useFontListener: fontListenerHook = ({ - fontNames, - interval, - timeout, - scope, -}) => { - const [hasLoaded, setHasLoaded] = useState(false) - const [loadedFonts, setLoadedFonts] = useState([]) - const [intervalId, setIntervalId] = useState(-1) - const attempts = useRef(Math.floor(timeout / interval)) - - const hasFonts = fontNames && Boolean(fontNames.length) - - const pendingFonts = useMemo( - () => fontNames.filter(fontName => !loadedFonts.includes(fontName)), - [loadedFonts, fontNames] - ) - const targetElement = useMemo( - () => (scope === "html" ? "documentElement" : "body"), - [scope] - ) - - const apiAvailable = "fonts" in document - - useEffect(() => { - if (!apiAvailable) { - handleApiError("Font loading API not available") - return - } - - if (hasFonts && apiAvailable && !hasLoaded && intervalId < 0) { - const id = window.setInterval(isFontLoaded, interval) - setIntervalId(id) - } - }, [hasFonts, hasLoaded, intervalId, apiAvailable]) - - useEffect(() => { - if (hasLoaded && intervalId > 0) { - clearInterval(intervalId) - } - }, [hasLoaded, intervalId]) - - function errorFallback() { - setHasLoaded(true) - setLoadedFonts(fontNames) - fontNames.forEach(addClassName) - } - - function handleApiError(error) { - console.info(`document.fonts API error: ${error}`) - console.info(`Replacing fonts instantly. FOUT handling failed.`) - errorFallback() - } - - function addClassName(fontName) { - document[targetElement].classList.add(`wf-${kebabCase(fontName)}--loaded`) - } - - function isFontLoaded() { - const loaded = [] - attempts.current = attempts.current - 1 - - if (attempts.current < 0) { - handleApiError("Interval timeout reached, maybe due to slow connection.") - } - - const fontsLoading = pendingFonts.map(fontName => { - let hasLoaded = false - try { - hasLoaded = document.fonts.check(`12px '${fontName}'`) - } catch (error) { - handleApiError(error) - return - } - - if (hasLoaded) { - addClassName(fontName) - loaded.push(fontName) - } - - return hasLoaded - }) - - const allFontsLoaded = fontsLoading.every(font => font) - - if (Boolean(loaded.length)) { - setLoadedFonts(loaded) - } - - if (allFontsLoaded) { - setHasLoaded(true) - } - } -} diff --git a/package.json b/package.json index ea345ac..edcd2bf 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "url": "https://github.com/codeAdrian/gatsby-omni-font-loader" }, "peerDependencies": { - "gatsby": "^2.0.0 || ^3.0.0 || ^4.0.0", + "gatsby": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", "react-helmet": ">=6.0.0" } } diff --git a/utils/fontListener.ts b/utils/fontListener.ts new file mode 100644 index 0000000..b27e060 --- /dev/null +++ b/utils/fontListener.ts @@ -0,0 +1,52 @@ +import { kebabCase } from "../utils"; + +declare var document: { fonts: any }; + +export const fontListener = ({ fontNames, scope }) => { + const hasFonts = fontNames && Boolean(fontNames.length); + const targetElement = scope === "html" ? "documentElement" : "body"; + const apiAvailable = "fonts" in document; + + function handleLoadComplete() { + addClassName("all"); + } + + function handleFontLoad(fontName: string) { + addClassName(fontName); + } + + function fontMapper(fontName) { + return document.fonts + .load(`1rem ${fontName}`) + .then(handleFontLoad) + .catch(errorFallback); + } + + function loadFonts() { + const fonts = fontNames.map(fontMapper); + Promise.all(fonts).then(handleLoadComplete).catch(errorFallback); + } + + function errorFallback() { + fontNames.forEach(addClassName); + } + + function handleApiError(error) { + console.info(`document.fonts API error: ${error}`); + console.info(`Replacing fonts instantly. FOUT handling failed.`); + errorFallback(); + } + + function addClassName(fontName) { + document[targetElement].classList.add(`wf-${kebabCase(fontName)}`); + } + + if (!apiAvailable) { + handleApiError("Font loading API not available"); + return; + } + + if (hasFonts && apiAvailable) { + loadFonts(); + } +};