Skip to content

Commit

Permalink
Add resize support to box when changing font-face on display/edit mode (
Browse files Browse the repository at this point in the history
#1275)

* Add resize support to box when changing font-face on display/edit mode

* Cover unicode chars, font-weight, font-style

* Add ((MULTIPLE)) support proposal

* Improve docs.

* Fix problematic rest approach.

* Add fontSize support. Remove content support. Add a better support for MULTIPLE. Improve useLoadFontFiles.js

* Update textStyle tests

* Force display=auto on @font-face declaration while loading fonts from Google Fonts.

* Fix typo

* Add codecov to useLoadFontFiles

* Update tests to match new APIs proposed after merge.

* Address PR reviews

* Address some PR reviews.

* Adjustments after #1323 merge. Revert content as parameter to load font (To address #923).

* Address recent PR review

* Improve code performance/reusability. Thanks @dvoytenko

* Clean up promise logic a bit

Co-authored-by: Pascal Birchler <pascalb@google.com>
Co-authored-by: Morten Barklund <morten.barklund@xwp.co>
  • Loading branch information
3 people committed May 19, 2020
1 parent d2a9a76 commit bc47950
Show file tree
Hide file tree
Showing 8 changed files with 268 additions and 54 deletions.
87 changes: 67 additions & 20 deletions assets/src/edit-story/app/font/actions/useLoadFontFiles.js
Expand Up @@ -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 <link> element to the <head> 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;
Expand Down
83 changes: 83 additions & 0 deletions 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();
});
});
14 changes: 11 additions & 3 deletions assets/src/edit-story/components/library/text/fontPreview.js
Expand Up @@ -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;

Expand All @@ -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 (
<Preview onClick={onClick}>
Expand All @@ -80,6 +87,7 @@ FontPreview.propTypes = {
font: FontPropType,
fontSize: PropTypes.number,
fontWeight: PropTypes.number,
content: PropTypes.string,
onClick: PropTypes.func,
};

Expand Down
17 changes: 9 additions & 8 deletions assets/src/edit-story/components/panels/test/textStyle.js
Expand Up @@ -73,6 +73,7 @@ function Wrapper({ children }) {
],
},
actions: {
maybeEnqueueFontStyle: () => Promise.resolve(),
getFontByName: () => ({
name: 'Neu Font',
value: 'Neu Font',
Expand Down Expand Up @@ -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: {
Expand All @@ -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(
Expand All @@ -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: '' });
});
});
Expand Down
54 changes: 40 additions & 14 deletions assets/src/edit-story/components/panels/textStyle/font.js
Expand Up @@ -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';

Expand All @@ -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 (
<>
Expand All @@ -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
);
Expand All @@ -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);
}}
/>
<Space />
</>
Expand Down

0 comments on commit bc47950

Please sign in to comment.