diff --git a/src/containers/gui.jsx b/src/containers/gui.jsx index ea705596f58c60d7a9a4246a1a2e6e929d3ebf4f..49b29672da7f6a9c459520bd62fecce3fe8d7d91 100644 --- a/src/containers/gui.jsx +++ b/src/containers/gui.jsx @@ -20,6 +20,7 @@ import { closeBackdropLibrary } from '../reducers/modals'; +import FontLoaderHOC from '../lib/font-loader-hoc.jsx'; import ProjectFetcherHOC from '../lib/project-fetcher-hoc.jsx'; import ProjectSaverHOC from '../lib/project-saver-hoc.jsx'; import vmListenerHOC from '../lib/vm-listener-hoc.jsx'; @@ -132,6 +133,7 @@ const ConnectedGUI = connect( // ability to compose reducers. const WrappedGui = compose( ErrorBoundaryHOC('Top Level App'), + FontLoaderHOC, ProjectFetcherHOC, ProjectSaverHOC, vmListenerHOC, diff --git a/src/lib/font-loader-hoc.jsx b/src/lib/font-loader-hoc.jsx new file mode 100644 index 0000000000000000000000000000000000000000..cbb7856c31fb3c95c3e8990d41e2cbd21922330f --- /dev/null +++ b/src/lib/font-loader-hoc.jsx @@ -0,0 +1,61 @@ +import React from 'react'; + +/* Higher Order Component to provide behavior for loading fonts. + * @param {React.Component} WrappedComponent component to receive fontsLoaded prop + * @returns {React.Component} component with font loading behavior + */ +const FontLoaderHOC = function (WrappedComponent) { + class FontLoaderComponent extends React.Component { + constructor (props) { + super(props); + this.state = { + fontsLoaded: false + }; + } + componentDidMount () { + const getFontPromises = () => { + const fontPromises = []; + // Browsers that support the font loader interface have an iterable document.fonts.values() + // Firefox has a mocked out object that doesn't actually implement iterable, which is why + // the deep safety check is necessary. + if (document.fonts && + typeof document.fonts.values === 'function' && + typeof document.fonts.values()[Symbol.iterator] === 'function') { + for (const fontFace of document.fonts.values()) { + fontPromises.push(fontFace.loaded); + fontFace.load(); + } + } + return fontPromises; + }; + // Font promises must be gathered after the document is loaded, because on Mac Chrome, the promise + // objects get replaced and the old ones never resolve. + if (document.readyState === 'complete') { + Promise.all(getFontPromises()).then(() => { + this.setState({fontsLoaded: true}); + }); + } else { + document.onreadystatechange = () => { + if (document.readyState !== 'complete') return; + document.onreadystatechange = null; + Promise.all(getFontPromises()).then(() => { + this.setState({fontsLoaded: true}); + }); + }; + } + } + render () { + return ( + <WrappedComponent + fontsLoaded={this.state.fontsLoaded} + {...this.props} + /> + ); + } + } + return FontLoaderComponent; +}; + +export { + FontLoaderHOC as default +}; diff --git a/src/lib/vm-manager-hoc.jsx b/src/lib/vm-manager-hoc.jsx index 449bff0eec2399b6792bda68b2b65c313a69e083..56bd7eabf74ddee9a6f0f714b9b12a25a45ef2f0 100644 --- a/src/lib/vm-manager-hoc.jsx +++ b/src/lib/vm-manager-hoc.jsx @@ -38,7 +38,10 @@ const vmManagerHOC = function (WrappedComponent) { this.props.vm.initialized = true; } componentDidUpdate (prevProps) { - if (this.props.isLoadingWithId && !prevProps.isLoadingWithId) { + // if project is in loading state, AND fonts are loaded, + // and they weren't both that way until now... load project! + if (this.props.isLoadingWithId && this.props.fontsLoaded && + (!prevProps.isLoadingWithId || !prevProps.fontsLoaded)) { this.loadProject(this.props.projectData, this.props.loadingState); } } @@ -56,6 +59,7 @@ const vmManagerHOC = function (WrappedComponent) { render () { const { /* eslint-disable no-unused-vars */ + fontsLoaded, onLoadedProject: onLoadedProjectProp, projectData, projectId, @@ -65,10 +69,6 @@ const vmManagerHOC = function (WrappedComponent) { vm, ...componentProps } = this.props; - // don't display anything until we have data loaded - if (!this.props.projectData) { - return null; - } return ( <WrappedComponent errorMessage={this.state.errorMessage} @@ -82,6 +82,7 @@ const vmManagerHOC = function (WrappedComponent) { } VMManager.propTypes = { + fontsLoaded: PropTypes.bool, isLoadingWithId: PropTypes.bool, loadingState: PropTypes.oneOf(LoadingStates), onLoadedProject: PropTypes.func, diff --git a/test/unit/util/vm-manager-hoc.test.jsx b/test/unit/util/vm-manager-hoc.test.jsx index 0bcb8f4b8ea8a1d224af62494a95dd3f9f4720b0..39015a4ded01098f16cbb86d8ca80d568bf1e9b1 100644 --- a/test/unit/util/vm-manager-hoc.test.jsx +++ b/test/unit/util/vm-manager-hoc.test.jsx @@ -60,6 +60,7 @@ describe('VMManagerHOC', () => { const WrappedComponent = vmManagerHOC(Component); const mounted = mount( <WrappedComponent + fontsLoaded isLoadingWithId={false} store={store} vm={vm} @@ -75,27 +76,46 @@ describe('VMManagerHOC', () => { // nextTick needed since vm.loadProject is async, and we have to wait for it :/ process.nextTick(() => expect(mockedOnLoadedProject).toHaveBeenLastCalledWith(LoadingState.LOADING_VM_WITH_ID)); }); - test('if there is projectData, the child is rendered', () => { + test('if the fontsLoaded prop becomes true, it loads project data into the vm', () => { + vm.loadProject = jest.fn(() => Promise.resolve()); + const mockedOnLoadedProject = jest.fn(); const Component = () => <div />; const WrappedComponent = vmManagerHOC(Component); const mounted = mount( <WrappedComponent - projectData="100" + isLoadingWithId store={store} vm={vm} + onLoadedProject={mockedOnLoadedProject} /> ); - expect(mounted.find('div').length).toBe(1); + mounted.setProps({ + fontsLoaded: true, + loadingState: LoadingState.LOADING_VM_WITH_ID, + projectData: '100' + }); + expect(vm.loadProject).toHaveBeenLastCalledWith('100'); + // nextTick needed since vm.loadProject is async, and we have to wait for it :/ + process.nextTick(() => expect(mockedOnLoadedProject).toHaveBeenLastCalledWith(LoadingState.LOADING_VM_WITH_ID)); }); - test('if there is no projectData, nothing is rendered', () => { + test('if the fontsLoaded prop is false, project data is never loaded', () => { + vm.loadProject = jest.fn(() => Promise.resolve()); + const mockedOnLoadedProject = jest.fn(); const Component = () => <div />; const WrappedComponent = vmManagerHOC(Component); const mounted = mount( <WrappedComponent + isLoadingWithId store={store} vm={vm} + onLoadedProject={mockedOnLoadedProject} /> ); - expect(mounted.find('div').length).toBe(0); + mounted.setProps({ + loadingState: LoadingState.LOADING_VM_WITH_ID, + projectData: '100' + }); + expect(vm.loadProject).toHaveBeenCalledTimes(0); + process.nextTick(() => expect(mockedOnLoadedProject).toHaveBeenCalledTimes(0)); }); });