diff --git a/assets/src/edit-story/app/font/actions/useLoadFontFiles.js b/assets/src/edit-story/app/font/actions/useLoadFontFiles.js
index f3d8c707054f..c4879dc95856 100644
--- a/assets/src/edit-story/app/font/actions/useLoadFontFiles.js
+++ b/assets/src/edit-story/app/font/actions/useLoadFontFiles.js
@@ -25,38 +25,85 @@ import { useCallback } from 'react';
import cleanForSlug from '../../../utils/cleanForSlug';
import getGoogleFontURL from '../../../utils/getGoogleFontURL';
+/**
+ * This is a utility ensure that Promise.all return ONLY when all promises are processed.
+ *
+ * @param {Promise} promise Promise to be processed
+ * @return {Promise} Return a rejected or fulfilled Promise
+ */
+const reflect = (promise) => {
+ return promise.then(
+ (v) => ({ v, status: 'fulfilled' }),
+ (e) => ({ e, status: 'rejected' })
+ );
+};
+
function useLoadFontFiles() {
/**
* Adds a element to the
for a given font in case there is none yet.
*
* Allows dynamically enqueuing font styles when needed.
*
- * @param {string} name Font name.
+ * @param {Array} fonts An array of fonts properties to create a valid FontFaceSet to inject and preload a font-face
+ * @return {Promise} Returns fonts loaded promise
*/
- const maybeEnqueueFontStyle = useCallback(({ family, service, variants }) => {
- if (!family || service !== 'fonts.google.com') {
- return;
- }
+ const maybeEnqueueFontStyle = useCallback((fonts) => {
+ return Promise.all(
+ fonts
+ .map(
+ async ({
+ font: { family, service, variants },
+ fontWeight,
+ fontStyle,
+ content,
+ }) => {
+ if (!family || service !== 'fonts.google.com') {
+ return null;
+ }
+
+ const handle = cleanForSlug(family);
+ const elementId = `${handle}-css`;
+ const fontFaceSet = `
+ ${fontStyle || ''} ${fontWeight || ''} 0 '${family}'
+ `.trim();
+
+ const hasFontLink = () => document.getElementById(elementId);
- const handle = cleanForSlug(family);
- const id = `${handle}-css`;
- const element = document.getElementById(id);
+ const appendFontLink = () => {
+ return new Promise((resolve, reject) => {
+ const src = getGoogleFontURL([{ family, variants }], 'auto');
+ const fontStylesheet = document.createElement('link');
+ fontStylesheet.id = elementId;
+ fontStylesheet.href = src;
+ fontStylesheet.rel = 'stylesheet';
+ fontStylesheet.type = 'text/css';
+ fontStylesheet.media = 'all';
+ fontStylesheet.crossOrigin = 'anonymous';
+ fontStylesheet.addEventListener('load', () => resolve());
+ fontStylesheet.addEventListener('error', (e) => reject(e));
+ document.head.appendChild(fontStylesheet);
+ });
+ };
- if (element) {
- return;
- }
+ const ensureFontLoaded = () => {
+ if (!document?.fonts) {
+ return Promise.resolve();
+ }
- const src = getGoogleFontURL([{ family, variants }]);
+ return document.fonts
+ .load(fontFaceSet, content || '')
+ .then(() => document.fonts.check(fontFaceSet, content || ''));
+ };
- const fontStylesheet = document.createElement('link');
- fontStylesheet.id = id;
- fontStylesheet.href = src;
- fontStylesheet.rel = 'stylesheet';
- fontStylesheet.type = 'text/css';
- fontStylesheet.media = 'all';
- fontStylesheet.crossOrigin = 'anonymous';
+ if (!hasFontLink()) {
+ await appendFontLink();
+ }
- document.head.appendChild(fontStylesheet);
+ return ensureFontLoaded();
+ }
+ )
+ .map(reflect)
+ );
}, []);
return maybeEnqueueFontStyle;
diff --git a/assets/src/edit-story/app/font/test/actions/useLoadFontFiles.js b/assets/src/edit-story/app/font/test/actions/useLoadFontFiles.js
new file mode 100644
index 000000000000..be34c80efb69
--- /dev/null
+++ b/assets/src/edit-story/app/font/test/actions/useLoadFontFiles.js
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * External dependencies
+ */
+import { renderHook } from '@testing-library/react-hooks';
+
+/**
+ * Internal dependencies
+ */
+import useLoadFontFiles from '../../actions/useLoadFontFiles';
+
+const DEFAULT_FONT = {
+ font: {
+ family: 'Font',
+ service: 'fonts.google.com',
+ },
+ fontWeight: 400,
+ fontStyle: 'normal',
+ content: 'Fill in some text',
+};
+
+describe('useLoadFontFiles', () => {
+ beforeEach(() => {
+ const el = document.getElementById('font-css');
+ if (el) {
+ el.remove();
+ }
+ });
+
+ it('maybeEnqueueFontStyle', () => {
+ expect(document.getElementById('font-css')).toBeNull();
+
+ renderHook(async () => {
+ const maybeEnqueueFontStyle = useLoadFontFiles();
+
+ await maybeEnqueueFontStyle([DEFAULT_FONT]);
+ });
+
+ expect(document.getElementById('font-css')).toBeDefined();
+ });
+
+ it('maybeEnqueueFontStyle skip', () => {
+ expect(document.getElementById('font-css')).toBeNull();
+
+ renderHook(async () => {
+ const maybeEnqueueFontStyle = useLoadFontFiles();
+
+ await maybeEnqueueFontStyle([
+ { ...DEFAULT_FONT, font: { ...DEFAULT_FONT.font, service: 'abcd' } },
+ ]);
+ });
+
+ expect(document.getElementById('font-css')).toBeNull();
+ });
+
+ it('maybeEnqueueFontStyle reflect', () => {
+ expect(document.getElementById('font-css')).toBeNull();
+
+ renderHook(async () => {
+ const maybeEnqueueFontStyle = useLoadFontFiles();
+
+ await maybeEnqueueFontStyle([{}, DEFAULT_FONT]);
+ });
+
+ expect(document.querySelectorAll('link')).toHaveLength(1);
+ expect(document.getElementById('font-css')).toBeDefined();
+ });
+});
diff --git a/assets/src/edit-story/components/library/text/fontPreview.js b/assets/src/edit-story/components/library/text/fontPreview.js
index 91c1b323b0b3..972da748fb7b 100644
--- a/assets/src/edit-story/components/library/text/fontPreview.js
+++ b/assets/src/edit-story/components/library/text/fontPreview.js
@@ -28,6 +28,7 @@ import { useEffect } from 'react';
import { useFont } from '../../../app';
import { ALLOWED_EDITOR_PAGE_WIDTHS, PAGE_WIDTH } from '../../../constants';
import { FontPropType } from '../../../types';
+import stripHTML from '../../../utils/stripHTML';
const PREVIEW_EM_SCALE = ALLOWED_EDITOR_PAGE_WIDTHS[0] / PAGE_WIDTH;
@@ -53,14 +54,20 @@ const Text = styled.span`
color: ${({ theme }) => theme.colors.fg.v1};
`;
-function FontPreview({ title, font, fontSize, fontWeight, onClick }) {
+function FontPreview({ title, font, fontSize, fontWeight, content, onClick }) {
const {
actions: { maybeEnqueueFontStyle },
} = useFont();
useEffect(() => {
- maybeEnqueueFontStyle(font);
- }, [font, maybeEnqueueFontStyle]);
+ maybeEnqueueFontStyle([
+ {
+ font,
+ fontWeight,
+ content: stripHTML(content),
+ },
+ ]);
+ }, [font, fontWeight, content, maybeEnqueueFontStyle]);
return (
@@ -80,6 +87,7 @@ FontPreview.propTypes = {
font: FontPropType,
fontSize: PropTypes.number,
fontWeight: PropTypes.number,
+ content: PropTypes.string,
onClick: PropTypes.func,
};
diff --git a/assets/src/edit-story/components/panels/test/textStyle.js b/assets/src/edit-story/components/panels/test/textStyle.js
index bfa5dde71a9d..22fc5f57c0e2 100644
--- a/assets/src/edit-story/components/panels/test/textStyle.js
+++ b/assets/src/edit-story/components/panels/test/textStyle.js
@@ -73,6 +73,7 @@ function Wrapper({ children }) {
],
},
actions: {
+ maybeEnqueueFontStyle: () => Promise.resolve(),
getFontByName: () => ({
name: 'Neu Font',
value: 'Neu Font',
@@ -420,9 +421,9 @@ describe('Panels/TextStyle', () => {
});
describe('FontControls', () => {
- it('should select font', () => {
+ it('should select font', async () => {
const { pushUpdate } = renderTextStyle([textElement]);
- act(() => controls.font.onChange('Neu Font'));
+ await act(() => controls.font.onChange('Neu Font'));
expect(pushUpdate).toHaveBeenCalledWith(
{
font: {
@@ -441,9 +442,9 @@ describe('Panels/TextStyle', () => {
);
});
- it('should select font weight', () => {
+ it('should select font weight', async () => {
const { pushUpdate } = renderTextStyle([textElement]);
- act(() => controls['font.weight'].onChange('300'));
+ await act(() => controls['font.weight'].onChange('300'));
const updatingFunction = pushUpdate.mock.calls[0][0];
const resultOfUpdating = updatingFunction({ content: 'Hello world' });
expect(resultOfUpdating).toStrictEqual(
@@ -454,17 +455,17 @@ describe('Panels/TextStyle', () => {
);
});
- it('should select font size', () => {
+ it('should select font size', async () => {
const { getByTestId, pushUpdate } = renderTextStyle([textElement]);
const input = getByTestId('font.size');
- fireEvent.change(input, { target: { value: '32' } });
+ await fireEvent.change(input, { target: { value: '32' } });
expect(pushUpdate).toHaveBeenCalledWith({ fontSize: 32 });
});
- it('should select font size to empty value', () => {
+ it('should select font size to empty value', async () => {
const { getByTestId, pushUpdate } = renderTextStyle([textElement]);
const input = getByTestId('font.size');
- fireEvent.change(input, { target: { value: '' } });
+ await fireEvent.change(input, { target: { value: '' } });
expect(pushUpdate).toHaveBeenCalledWith({ fontSize: '' });
});
});
diff --git a/assets/src/edit-story/components/panels/textStyle/font.js b/assets/src/edit-story/components/panels/textStyle/font.js
index e199668c26fd..f47553fb85c6 100644
--- a/assets/src/edit-story/components/panels/textStyle/font.js
+++ b/assets/src/edit-story/components/panels/textStyle/font.js
@@ -34,6 +34,7 @@ import { PAGE_HEIGHT } from '../../../constants';
import { useFont } from '../../../app/font';
import { getCommonValue } from '../utils';
import objectPick from '../../../utils/objectPick';
+import stripHTML from '../../../utils/stripHTML';
import useRichTextFormatting from './useRichTextFormatting';
import getFontWeights from './getFontWeights';
@@ -54,18 +55,19 @@ function FontControls({ selectedElements, pushUpdate }) {
const fontSize = getCommonValue(selectedElements, 'fontSize');
const {
- textInfo: { fontWeight },
+ textInfo: { fontWeight, isItalic },
handlers: { handleSelectFontWeight },
} = useRichTextFormatting(selectedElements, pushUpdate);
const {
state: { fonts },
- actions: { getFontByName },
+ actions: { maybeEnqueueFontStyle, getFontByName },
} = useFont();
const fontWeights = useMemo(() => getFontWeights(getFontByName(fontFamily)), [
getFontByName,
fontFamily,
]);
+ const fontStyle = isItalic ? 'italic' : 'normal';
return (
<>
@@ -76,21 +78,33 @@ function FontControls({ selectedElements, pushUpdate }) {
ariaLabel={__('Font family', 'web-stories')}
options={fonts}
value={fontFamily}
- onChange={(value) => {
+ onChange={async (value) => {
const fontObj = fonts.find((item) => item.value === value);
+ const newFont = {
+ family: value,
+ ...objectPick(fontObj, [
+ 'service',
+ 'fallbacks',
+ 'weights',
+ 'styles',
+ 'variants',
+ ]),
+ };
+
+ await maybeEnqueueFontStyle(
+ selectedElements.map(({ content }) => {
+ return {
+ font: newFont,
+ fontStyle,
+ fontWeight,
+ content: stripHTML(content),
+ };
+ })
+ );
pushUpdate(
{
- font: {
- family: value,
- ...objectPick(fontObj, [
- 'service',
- 'fallbacks',
- 'weights',
- 'styles',
- 'variants',
- ]),
- },
+ font: newFont,
},
true
);
@@ -107,7 +121,19 @@ function FontControls({ selectedElements, pushUpdate }) {
placeholder={__('(multiple)', 'web-stories')}
options={fontWeights}
value={fontWeight}
- onChange={handleSelectFontWeight}
+ onChange={async (value) => {
+ await maybeEnqueueFontStyle(
+ selectedElements.map(({ font, content }) => {
+ return {
+ font,
+ fontStyle,
+ fontWeight: parseInt(value),
+ content: stripHTML(content),
+ };
+ })
+ );
+ handleSelectFontWeight(value);
+ }}
/>
>
diff --git a/assets/src/edit-story/components/panels/textStyle/textStyle.js b/assets/src/edit-story/components/panels/textStyle/textStyle.js
index ba7edbfbb530..2504a5a35890 100644
--- a/assets/src/edit-story/components/panels/textStyle/textStyle.js
+++ b/assets/src/edit-story/components/panels/textStyle/textStyle.js
@@ -40,6 +40,8 @@ import { ReactComponent as BoldIcon } from '../../../icons/bold_icon.svg';
import { ReactComponent as ItalicIcon } from '../../../icons/italic_icon.svg';
import { ReactComponent as UnderlineIcon } from '../../../icons/underline_icon.svg';
import { getCommonValue } from '../utils';
+import { useFont } from '../../../app/font';
+import stripHTML from '../../../utils/stripHTML';
import useRichTextFormatting from './useRichTextFormatting';
const BoxedNumeric = styled(Numeric)`
@@ -62,11 +64,14 @@ const Space = styled.div`
`;
function StylePanel({ selectedElements, pushUpdate }) {
+ const {
+ actions: { maybeEnqueueFontStyle },
+ } = useFont();
const textAlign = getCommonValue(selectedElements, 'textAlign');
const lineHeight = getCommonValue(selectedElements, 'lineHeight');
const {
- textInfo: { isBold, isItalic, isUnderline, letterSpacing },
+ textInfo: { isBold, isItalic, isUnderline, letterSpacing, fontWeight },
handlers: {
handleClickBold,
handleClickItalic,
@@ -137,7 +142,19 @@ function StylePanel({ selectedElements, pushUpdate }) {
value={isItalic}
iconWidth={10}
iconHeight={10}
- onChange={handleClickItalic}
+ onChange={async (value) => {
+ await maybeEnqueueFontStyle(
+ selectedElements.map(({ font, content }) => {
+ return {
+ font,
+ fontStyle: value ? 'italic' : 'normal',
+ fontWeight,
+ content: stripHTML(content),
+ };
+ })
+ );
+ handleClickItalic(value);
+ }}
/>
}
diff --git a/assets/src/edit-story/elements/text/display.js b/assets/src/edit-story/elements/text/display.js
index b22752b391bf..a9e4b2ba09d5 100644
--- a/assets/src/edit-story/elements/text/display.js
+++ b/assets/src/edit-story/elements/text/display.js
@@ -34,8 +34,12 @@ import {
import StoryPropTypes from '../../types';
import { BACKGROUND_TEXT_MODE } from '../../constants';
import { useTransformHandler } from '../../components/transform';
-import { getHTMLFormatters } from '../../components/richText/htmlManipulation';
+import {
+ getHTMLFormatters,
+ getHTMLInfo,
+} from '../../components/richText/htmlManipulation';
import createSolid from '../../utils/createSolid';
+import stripHTML from '../../utils/stripHTML';
import { getHighlightLineheight, generateParagraphTextStyle } from './util';
const HighlightWrapperElement = styled.div`
@@ -101,6 +105,14 @@ function TextDisplay({
} = useUnits();
const { font } = rest;
+ const fontFaceSetConfigs = useMemo(() => {
+ const htmlInfo = getHTMLInfo(content);
+ return {
+ fontStyle: htmlInfo.isItalic ? 'italic' : 'normal',
+ fontWeight: htmlInfo.fontWeight,
+ content: stripHTML(content),
+ };
+ }, [content]);
const props = {
font,
@@ -115,10 +127,9 @@ function TextDisplay({
const {
actions: { maybeEnqueueFontStyle },
} = useFont();
-
useEffect(() => {
- maybeEnqueueFontStyle(font);
- }, [font, maybeEnqueueFontStyle]);
+ maybeEnqueueFontStyle([{ ...fontFaceSetConfigs, font }]);
+ }, [font, fontFaceSetConfigs, maybeEnqueueFontStyle]);
useTransformHandler(id, (transform) => {
const target = ref.current;
diff --git a/assets/src/edit-story/elements/text/edit.js b/assets/src/edit-story/elements/text/edit.js
index 9a3bf0446cbe..198290669400 100644
--- a/assets/src/edit-story/elements/text/edit.js
+++ b/assets/src/edit-story/elements/text/edit.js
@@ -18,13 +18,20 @@
* External dependencies
*/
import styled from 'styled-components';
-import { useEffect, useLayoutEffect, useRef, useCallback } from 'react';
+import {
+ useEffect,
+ useLayoutEffect,
+ useRef,
+ useCallback,
+ useMemo,
+} from 'react';
/**
* Internal dependencies
*/
import { useStory, useFont } from '../../app';
import RichTextEditor from '../../components/richText/editor';
+import { getHTMLInfo } from '../../components/richText/htmlManipulation';
import { useUnits } from '../../units';
import {
elementFillContent,
@@ -36,6 +43,7 @@ import StoryPropTypes from '../../types';
import { BACKGROUND_TEXT_MODE } from '../../constants';
import useUnmount from '../../utils/useUnmount';
import createSolid from '../../utils/createSolid';
+import stripHTML from '../../utils/stripHTML';
import calcRotatedResizeOffset from '../../utils/calcRotatedResizeOffset';
import { generateParagraphTextStyle, getHighlightLineheight } from './util';
@@ -86,6 +94,14 @@ function TextEdit({
box: { x, y, height, rotationAngle },
}) {
const { font } = rest;
+ const fontFaceSetConfigs = useMemo(() => {
+ const htmlInfo = getHTMLInfo(content);
+ return {
+ fontStyle: htmlInfo.isItalic ? 'italic' : 'normal',
+ fontWeight: htmlInfo.fontWeight,
+ content: stripHTML(content),
+ };
+ }, [content]);
const {
actions: { dataToEditorX, dataToEditorY, editorToDataX, editorToDataY },
@@ -193,8 +209,13 @@ function TextEdit({
useEffect(handleResize, [elementHeight]);
useEffect(() => {
- maybeEnqueueFontStyle(font);
- }, [font, maybeEnqueueFontStyle]);
+ maybeEnqueueFontStyle([
+ {
+ ...fontFaceSetConfigs,
+ font,
+ },
+ ]);
+ }, [font, fontFaceSetConfigs, maybeEnqueueFontStyle]);
return (