Skip to content

Commit c16690b

Browse files
author
Luke Bowerman
authored
feat(FontFaceLoader): new component to support flexible font loading (#1954)
* feat(FontFaceLoader): new component to support flexible font loading - Leverages `react-helmet-async` to add `<style>` tag for loading fontSources specified in theme.fontSources - Refactors GoogleFontLoader to instead generate `FontSource` and leverage `FontFaceLoader` * Fix bad logic within ComponentsProvider * Coverage for ComponentsProvider
1 parent a6765e2 commit c16690b

File tree

15 files changed

+394
-39
lines changed

15 files changed

+394
-39
lines changed

packages/components-providers/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"@looker/design-tokens": "^0.14.1",
2222
"i18next": "^19.8.7",
2323
"lodash": "^4.17.20",
24+
"react-helmet-async": "^1.0.7",
2425
"react-i18next": "11.8.8",
2526
"tabbable": "^5.1.4"
2627
},
@@ -29,6 +30,7 @@
2930
"@types/lodash": "^4.14.167",
3031
"@types/react": "^16.9.56",
3132
"@types/styled-components": "^5.1.5",
33+
"enzyme": "^3.11.0",
3234
"react": "^16.14.0",
3335
"react-is": "^16.13.1",
3436
"styled-components": "^5.2.1"
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
3+
MIT License
4+
5+
Copyright (c) 2021 Looker Data Sciences, Inc.
6+
7+
Permission is hereby granted, free of charge, to any person obtaining a copy
8+
of this software and associated documentation files (the "Software"), to deal
9+
in the Software without restriction, including without limitation the rights
10+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
copies of the Software, and to permit persons to whom the Software is
12+
furnished to do so, subject to the following conditions:
13+
14+
The above copyright notice and this permission notice shall be included in all
15+
copies or substantial portions of the Software.
16+
17+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23+
SOFTWARE.
24+
25+
*/
26+
27+
import React from 'react'
28+
import { renderWithTheme } from '@looker/components-test-utils'
29+
import { screen } from '@testing-library/react'
30+
import styled from 'styled-components'
31+
import { ComponentsProvider } from './ComponentsProvider'
32+
33+
const FauxParagraph = styled.p`
34+
background: ${({ theme }) => theme.colors.background};
35+
color: ${({ theme }) => theme.colors.key};
36+
font-family: ${({ theme }) => theme.fonts.body};
37+
`
38+
39+
describe('ComponentsProvider', () => {
40+
test('Nesting ignores parent values (not a desireable choice)', () => {
41+
const Test = () => {
42+
return (
43+
<ComponentsProvider
44+
themeCustomizations={{
45+
colors: { background: 'black' },
46+
}}
47+
>
48+
<FauxParagraph>1 layer</FauxParagraph>
49+
<ComponentsProvider
50+
themeCustomizations={{
51+
colors: { key: 'purple' },
52+
}}
53+
>
54+
<FauxParagraph>2 layer</FauxParagraph>
55+
</ComponentsProvider>
56+
</ComponentsProvider>
57+
)
58+
}
59+
60+
renderWithTheme(<Test />)
61+
62+
expect(screen.getByText('1 layer')).toHaveStyle('color:rgb(108, 67, 224)')
63+
expect(screen.getByText('1 layer')).toHaveStyle('background: black')
64+
expect(screen.getByText('2 layer')).toHaveStyle('color: purple')
65+
expect(screen.getByText('2 layer')).toHaveStyle(
66+
'background:rgb(255, 255, 255);'
67+
)
68+
})
69+
70+
test('loadGoogleFonts', () => {
71+
const Test = () => {
72+
return (
73+
<ComponentsProvider loadGoogleFonts>
74+
<FauxParagraph>Some Text</FauxParagraph>
75+
</ComponentsProvider>
76+
)
77+
}
78+
79+
renderWithTheme(<Test />)
80+
81+
expect(screen.getByText('Some Text')).toHaveStyle(
82+
"font-family: Roboto,'Noto Sans','Noto Sans JP','Noto Sans CJK KR','Noto Sans Arabic UI','Noto Sans Devanagari UI','Noto Sans Hebrew','Noto Sans Thai UI',Helvetica,Arial,sans-serif;"
83+
)
84+
})
85+
86+
test('ie11Support', () => {
87+
const Test = () => {
88+
return (
89+
<ComponentsProvider ie11Support>
90+
<FauxParagraph>Some Text</FauxParagraph>
91+
</ComponentsProvider>
92+
)
93+
}
94+
95+
renderWithTheme(<Test />)
96+
97+
expect(screen.getByText('Some Text')).toHaveStyle(
98+
"font-family: Roboto,'Noto Sans','Noto Sans JP','Noto Sans CJK KR','Noto Sans Arabic UI','Noto Sans Devanagari UI','Noto Sans Hebrew','Noto Sans Thai UI',Helvetica,Arial,sans-serif;"
99+
)
100+
})
101+
})

packages/components-providers/src/ComponentsProvider.tsx

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,18 @@
2727
import {
2828
generateTheme,
2929
GlobalStyle,
30-
GoogleFontsLoader,
3130
IEGlobalStyle,
31+
googleFontUrl,
3232
theme as defaultTheme,
3333
} from '@looker/design-tokens'
3434
import React, { FC, useMemo } from 'react'
35+
import { HelmetProvider } from 'react-helmet-async'
3536
import { FocusTrapProvider } from './FocusTrap'
3637
import { ScrollLockProvider } from './ScrollLock'
3738
import { useI18n, UseI18nProps } from './I18n'
3839
import { ThemeProvider, ThemeProviderProps } from './ThemeProvider'
3940
import { ExtendComponentsTheme } from './ExtendComponentsProvider'
41+
import { FontFaceLoader } from './FontFaceLoader'
4042

4143
export interface ComponentsProviderProps
4244
extends ThemeProviderProps,
@@ -47,6 +49,11 @@ export interface ComponentsProviderProps
4749
* @default true
4850
*/
4951
globalStyle?: boolean
52+
/**
53+
* Load any font faces specified on theme.fontSources
54+
* @default true
55+
*/
56+
loadFontSources?: boolean
5057
/**
5158
* Load fonts from the Google Fonts CDN if not already available
5259
* @default false
@@ -78,26 +85,41 @@ export const ComponentsProvider: FC<ComponentsProviderProps> = ({
7885
children,
7986
globalStyle = true,
8087
ie11Support = false,
88+
loadFontSources = true,
8189
loadGoogleFonts = false,
8290
locale,
8391
resources,
8492
themeCustomizations,
8593
...props
8694
}) => {
8795
const theme = useMemo(() => {
88-
return generateTheme(props.theme || defaultTheme, themeCustomizations)
89-
}, [props.theme, themeCustomizations])
96+
const draft = generateTheme(
97+
props.theme || defaultTheme,
98+
themeCustomizations
99+
)
100+
101+
if (loadGoogleFonts) {
102+
draft.fontSources = [
103+
...(draft.fontSources || []),
104+
{ url: googleFontUrl(draft) },
105+
]
106+
}
107+
108+
return draft
109+
}, [props.theme, loadGoogleFonts, themeCustomizations])
90110

91111
useI18n({ locale, resources })
92112

93113
return (
94-
<ThemeProvider {...props} theme={theme}>
95-
{globalStyle && <GlobalStyle />}
96-
{loadGoogleFonts && <GoogleFontsLoader />}
97-
{ie11Support && <IEGlobalStyle />}
98-
<FocusTrapProvider>
99-
<ScrollLockProvider>{children}</ScrollLockProvider>
100-
</FocusTrapProvider>
101-
</ThemeProvider>
114+
<HelmetProvider>
115+
<ThemeProvider {...props} theme={theme}>
116+
{globalStyle && <GlobalStyle />}
117+
{loadFontSources && <FontFaceLoader />}
118+
{ie11Support && <IEGlobalStyle />}
119+
<FocusTrapProvider>
120+
<ScrollLockProvider>{children}</ScrollLockProvider>
121+
</FocusTrapProvider>
122+
</ThemeProvider>
123+
</HelmetProvider>
102124
)
103125
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
3+
MIT License
4+
5+
Copyright (c) 2021 Looker Data Sciences, Inc.
6+
7+
Permission is hereby granted, free of charge, to any person obtaining a copy
8+
of this software and associated documentation files (the "Software"), to deal
9+
in the Software without restriction, including without limitation the rights
10+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
copies of the Software, and to permit persons to whom the Software is
12+
furnished to do so, subject to the following conditions:
13+
14+
The above copyright notice and this permission notice shall be included in all
15+
copies or substantial portions of the Software.
16+
17+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23+
SOFTWARE.
24+
25+
*/
26+
27+
import { mount } from 'enzyme'
28+
import React from 'react'
29+
import { HelmetProvider } from 'react-helmet-async'
30+
import { FontSources } from '@looker/design-tokens'
31+
import { DefaultTheme, ThemeProvider } from 'styled-components'
32+
import { fontFacesCSS, FontFaceLoader } from './FontFaceLoader'
33+
34+
HelmetProvider.canUseDOM = false
35+
36+
const fontSources: FontSources = [
37+
{ url: 'http//magic.com' },
38+
{ face: 'Curly', url: 'http//moe.com/curly.ttf' },
39+
]
40+
41+
describe('FontFaceLoader', () => {
42+
it('Font face with URL', () => {
43+
expect(fontFacesCSS([fontSources[1]])).toMatchInlineSnapshot(`
44+
"
45+
@font-face {
46+
font-family: Curly;
47+
src: url(http//moe.com/curly.ttf);
48+
}"
49+
`)
50+
})
51+
52+
it('URL only (Google font)', () => {
53+
expect(fontFacesCSS([fontSources[0]])).toMatchInlineSnapshot(`
54+
"
55+
@import url(http//magic.com);"
56+
`)
57+
})
58+
59+
it('Multiple fonts', () => {
60+
expect(fontFacesCSS(fontSources)).toMatchInlineSnapshot(`
61+
"
62+
@import url(http//magic.com);
63+
64+
@font-face {
65+
font-family: Curly;
66+
src: url(http//moe.com/curly.ttf);
67+
}"
68+
`)
69+
})
70+
71+
it('Does nothing if fontSource undefined', () => {
72+
const context = {} as any
73+
74+
mount(
75+
<HelmetProvider context={context}>
76+
<ThemeProvider theme={({} as unknown) as DefaultTheme}>
77+
<FontFaceLoader />
78+
</ThemeProvider>
79+
</HelmetProvider>
80+
)
81+
expect(context.helmet.style.toString()).toEqual('')
82+
})
83+
84+
it('Does nothing if fontSource empty', () => {
85+
const context = {} as any
86+
87+
mount(
88+
<HelmetProvider context={context}>
89+
<ThemeProvider
90+
theme={({ themeSources: [] } as unknown) as DefaultTheme}
91+
>
92+
<FontFaceLoader />
93+
</ThemeProvider>
94+
</HelmetProvider>
95+
)
96+
97+
expect(context.helmet.style.toString()).toEqual('')
98+
})
99+
100+
it('theme.fontSources has entries', () => {
101+
const context = {} as any
102+
103+
mount(
104+
<HelmetProvider context={context}>
105+
<ThemeProvider
106+
theme={
107+
({
108+
fontSources,
109+
} as unknown) as DefaultTheme
110+
}
111+
>
112+
<FontFaceLoader />
113+
</ThemeProvider>
114+
</HelmetProvider>
115+
)
116+
// expect(component.find('head').length).toEqual(0)
117+
expect(context.helmet.style.toString()).toMatchInlineSnapshot(`
118+
"<style data-rh=\\"true\\" type=\\"text/css\\">
119+
@import url(http//magic.com);
120+
121+
@font-face {
122+
font-family: Curly;
123+
src: url(http//moe.com/curly.ttf);
124+
}</style>"
125+
`)
126+
})
127+
})
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
3+
MIT License
4+
5+
Copyright (c) 2021 Looker Data Sciences, Inc.
6+
7+
Permission is hereby granted, free of charge, to any person obtaining a copy
8+
of this software and associated documentation files (the "Software"), to deal
9+
in the Software without restriction, including without limitation the rights
10+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
copies of the Software, and to permit persons to whom the Software is
12+
furnished to do so, subject to the following conditions:
13+
14+
The above copyright notice and this permission notice shall be included in all
15+
copies or substantial portions of the Software.
16+
17+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23+
SOFTWARE.
24+
25+
*/
26+
27+
import React, { useContext } from 'react'
28+
import { Helmet } from 'react-helmet-async'
29+
import { ThemeContext } from 'styled-components'
30+
import { FontSources } from '@looker/design-tokens'
31+
32+
export const fontFacesCSS = (fontSources: FontSources) =>
33+
fontSources
34+
.map(({ face, url }) => (face ? fontFace(face, url) : importFont(url)))
35+
.join('\n')
36+
37+
export const importFont = (url: string) => `
38+
@import url(${url});`
39+
40+
export const fontFace = (face: string, url: string) => `
41+
@font-face {
42+
font-family: ${face};
43+
src: url(${url});
44+
}`
45+
46+
/**
47+
* FontFaceLoader injects font @font-face imports into a style tag on the page's <HEAD>
48+
* Font sources are determined using the fontSources key on the theme
49+
*/
50+
export const FontFaceLoader = () => {
51+
const { fontSources } = useContext(ThemeContext)
52+
53+
if (!fontSources || fontSources.length === 0) return null
54+
55+
const css = fontFacesCSS(fontSources)
56+
57+
return (
58+
<Helmet>
59+
<style type="text/css">{css}</style>
60+
</Helmet>
61+
)
62+
}

packages/design-tokens/src/GoogleFontsLoader/index.ts renamed to packages/components-providers/src/FontFaceLoader/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,4 @@
2424
2525
*/
2626

27-
export { GoogleFontsLoader } from './GoogleFontsLoader'
27+
export { FontFaceLoader } from './FontFaceLoader'

0 commit comments

Comments
 (0)