Skip to content

Commit 41fc932

Browse files
authored
Merge pull request #3366 from fsih/preloadFonts
Preload fonts
2 parents c81e0e5 + 80bee42 commit 41fc932

File tree

4 files changed

+94
-10
lines changed

4 files changed

+94
-10
lines changed

src/containers/gui.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
closeBackdropLibrary
2121
} from '../reducers/modals';
2222

23+
import FontLoaderHOC from '../lib/font-loader-hoc.jsx';
2324
import ProjectFetcherHOC from '../lib/project-fetcher-hoc.jsx';
2425
import ProjectSaverHOC from '../lib/project-saver-hoc.jsx';
2526
import vmListenerHOC from '../lib/vm-listener-hoc.jsx';
@@ -132,6 +133,7 @@ const ConnectedGUI = connect(
132133
// ability to compose reducers.
133134
const WrappedGui = compose(
134135
ErrorBoundaryHOC('Top Level App'),
136+
FontLoaderHOC,
135137
ProjectFetcherHOC,
136138
ProjectSaverHOC,
137139
vmListenerHOC,

src/lib/font-loader-hoc.jsx

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import React from 'react';
2+
3+
/* Higher Order Component to provide behavior for loading fonts.
4+
* @param {React.Component} WrappedComponent component to receive fontsLoaded prop
5+
* @returns {React.Component} component with font loading behavior
6+
*/
7+
const FontLoaderHOC = function (WrappedComponent) {
8+
class FontLoaderComponent extends React.Component {
9+
constructor (props) {
10+
super(props);
11+
this.state = {
12+
fontsLoaded: false
13+
};
14+
}
15+
componentDidMount () {
16+
const getFontPromises = () => {
17+
const fontPromises = [];
18+
// Browsers that support the font loader interface have an iterable document.fonts.values()
19+
// Firefox has a mocked out object that doesn't actually implement iterable, which is why
20+
// the deep safety check is necessary.
21+
if (document.fonts &&
22+
typeof document.fonts.values === 'function' &&
23+
typeof document.fonts.values()[Symbol.iterator] === 'function') {
24+
for (const fontFace of document.fonts.values()) {
25+
fontPromises.push(fontFace.loaded);
26+
fontFace.load();
27+
}
28+
}
29+
return fontPromises;
30+
};
31+
// Font promises must be gathered after the document is loaded, because on Mac Chrome, the promise
32+
// objects get replaced and the old ones never resolve.
33+
if (document.readyState === 'complete') {
34+
Promise.all(getFontPromises()).then(() => {
35+
this.setState({fontsLoaded: true});
36+
});
37+
} else {
38+
document.onreadystatechange = () => {
39+
if (document.readyState !== 'complete') return;
40+
document.onreadystatechange = null;
41+
Promise.all(getFontPromises()).then(() => {
42+
this.setState({fontsLoaded: true});
43+
});
44+
};
45+
}
46+
}
47+
render () {
48+
return (
49+
<WrappedComponent
50+
fontsLoaded={this.state.fontsLoaded}
51+
{...this.props}
52+
/>
53+
);
54+
}
55+
}
56+
return FontLoaderComponent;
57+
};
58+
59+
export {
60+
FontLoaderHOC as default
61+
};

src/lib/vm-manager-hoc.jsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@ const vmManagerHOC = function (WrappedComponent) {
3838
this.props.vm.initialized = true;
3939
}
4040
componentDidUpdate (prevProps) {
41-
if (this.props.isLoadingWithId && !prevProps.isLoadingWithId) {
41+
// if project is in loading state, AND fonts are loaded,
42+
// and they weren't both that way until now... load project!
43+
if (this.props.isLoadingWithId && this.props.fontsLoaded &&
44+
(!prevProps.isLoadingWithId || !prevProps.fontsLoaded)) {
4245
this.loadProject(this.props.projectData, this.props.loadingState);
4346
}
4447
}
@@ -56,6 +59,7 @@ const vmManagerHOC = function (WrappedComponent) {
5659
render () {
5760
const {
5861
/* eslint-disable no-unused-vars */
62+
fontsLoaded,
5963
onLoadedProject: onLoadedProjectProp,
6064
projectData,
6165
projectId,
@@ -65,10 +69,6 @@ const vmManagerHOC = function (WrappedComponent) {
6569
vm,
6670
...componentProps
6771
} = this.props;
68-
// don't display anything until we have data loaded
69-
if (!this.props.projectData) {
70-
return null;
71-
}
7272
return (
7373
<WrappedComponent
7474
errorMessage={this.state.errorMessage}
@@ -82,6 +82,7 @@ const vmManagerHOC = function (WrappedComponent) {
8282
}
8383

8484
VMManager.propTypes = {
85+
fontsLoaded: PropTypes.bool,
8586
isLoadingWithId: PropTypes.bool,
8687
loadingState: PropTypes.oneOf(LoadingStates),
8788
onLoadedProject: PropTypes.func,

test/unit/util/vm-manager-hoc.test.jsx

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ describe('VMManagerHOC', () => {
6060
const WrappedComponent = vmManagerHOC(Component);
6161
const mounted = mount(
6262
<WrappedComponent
63+
fontsLoaded
6364
isLoadingWithId={false}
6465
store={store}
6566
vm={vm}
@@ -75,27 +76,46 @@ describe('VMManagerHOC', () => {
7576
// nextTick needed since vm.loadProject is async, and we have to wait for it :/
7677
process.nextTick(() => expect(mockedOnLoadedProject).toHaveBeenLastCalledWith(LoadingState.LOADING_VM_WITH_ID));
7778
});
78-
test('if there is projectData, the child is rendered', () => {
79+
test('if the fontsLoaded prop becomes true, it loads project data into the vm', () => {
80+
vm.loadProject = jest.fn(() => Promise.resolve());
81+
const mockedOnLoadedProject = jest.fn();
7982
const Component = () => <div />;
8083
const WrappedComponent = vmManagerHOC(Component);
8184
const mounted = mount(
8285
<WrappedComponent
83-
projectData="100"
86+
isLoadingWithId
8487
store={store}
8588
vm={vm}
89+
onLoadedProject={mockedOnLoadedProject}
8690
/>
8791
);
88-
expect(mounted.find('div').length).toBe(1);
92+
mounted.setProps({
93+
fontsLoaded: true,
94+
loadingState: LoadingState.LOADING_VM_WITH_ID,
95+
projectData: '100'
96+
});
97+
expect(vm.loadProject).toHaveBeenLastCalledWith('100');
98+
// nextTick needed since vm.loadProject is async, and we have to wait for it :/
99+
process.nextTick(() => expect(mockedOnLoadedProject).toHaveBeenLastCalledWith(LoadingState.LOADING_VM_WITH_ID));
89100
});
90-
test('if there is no projectData, nothing is rendered', () => {
101+
test('if the fontsLoaded prop is false, project data is never loaded', () => {
102+
vm.loadProject = jest.fn(() => Promise.resolve());
103+
const mockedOnLoadedProject = jest.fn();
91104
const Component = () => <div />;
92105
const WrappedComponent = vmManagerHOC(Component);
93106
const mounted = mount(
94107
<WrappedComponent
108+
isLoadingWithId
95109
store={store}
96110
vm={vm}
111+
onLoadedProject={mockedOnLoadedProject}
97112
/>
98113
);
99-
expect(mounted.find('div').length).toBe(0);
114+
mounted.setProps({
115+
loadingState: LoadingState.LOADING_VM_WITH_ID,
116+
projectData: '100'
117+
});
118+
expect(vm.loadProject).toHaveBeenCalledTimes(0);
119+
process.nextTick(() => expect(mockedOnLoadedProject).toHaveBeenCalledTimes(0));
100120
});
101121
});

0 commit comments

Comments
 (0)