Skip to content

Commit bc47950

Browse files
obetomunizswissspidyMorten Barklund
authored
Add resize support to box when changing font-face on display/edit mode (#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>
1 parent d2a9a76 commit bc47950

8 files changed

Lines changed: 268 additions & 54 deletions

File tree

assets/src/edit-story/app/font/actions/useLoadFontFiles.js

Lines changed: 67 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -25,38 +25,85 @@ import { useCallback } from 'react';
2525
import cleanForSlug from '../../../utils/cleanForSlug';
2626
import getGoogleFontURL from '../../../utils/getGoogleFontURL';
2727

28+
/**
29+
* This is a utility ensure that Promise.all return ONLY when all promises are processed.
30+
*
31+
* @param {Promise} promise Promise to be processed
32+
* @return {Promise} Return a rejected or fulfilled Promise
33+
*/
34+
const reflect = (promise) => {
35+
return promise.then(
36+
(v) => ({ v, status: 'fulfilled' }),
37+
(e) => ({ e, status: 'rejected' })
38+
);
39+
};
40+
2841
function useLoadFontFiles() {
2942
/**
3043
* Adds a <link> element to the <head> for a given font in case there is none yet.
3144
*
3245
* Allows dynamically enqueuing font styles when needed.
3346
*
34-
* @param {string} name Font name.
47+
* @param {Array} fonts An array of fonts properties to create a valid FontFaceSet to inject and preload a font-face
48+
* @return {Promise} Returns fonts loaded promise
3549
*/
36-
const maybeEnqueueFontStyle = useCallback(({ family, service, variants }) => {
37-
if (!family || service !== 'fonts.google.com') {
38-
return;
39-
}
50+
const maybeEnqueueFontStyle = useCallback((fonts) => {
51+
return Promise.all(
52+
fonts
53+
.map(
54+
async ({
55+
font: { family, service, variants },
56+
fontWeight,
57+
fontStyle,
58+
content,
59+
}) => {
60+
if (!family || service !== 'fonts.google.com') {
61+
return null;
62+
}
63+
64+
const handle = cleanForSlug(family);
65+
const elementId = `${handle}-css`;
66+
const fontFaceSet = `
67+
${fontStyle || ''} ${fontWeight || ''} 0 '${family}'
68+
`.trim();
69+
70+
const hasFontLink = () => document.getElementById(elementId);
4071

41-
const handle = cleanForSlug(family);
42-
const id = `${handle}-css`;
43-
const element = document.getElementById(id);
72+
const appendFontLink = () => {
73+
return new Promise((resolve, reject) => {
74+
const src = getGoogleFontURL([{ family, variants }], 'auto');
75+
const fontStylesheet = document.createElement('link');
76+
fontStylesheet.id = elementId;
77+
fontStylesheet.href = src;
78+
fontStylesheet.rel = 'stylesheet';
79+
fontStylesheet.type = 'text/css';
80+
fontStylesheet.media = 'all';
81+
fontStylesheet.crossOrigin = 'anonymous';
82+
fontStylesheet.addEventListener('load', () => resolve());
83+
fontStylesheet.addEventListener('error', (e) => reject(e));
84+
document.head.appendChild(fontStylesheet);
85+
});
86+
};
4487

45-
if (element) {
46-
return;
47-
}
88+
const ensureFontLoaded = () => {
89+
if (!document?.fonts) {
90+
return Promise.resolve();
91+
}
4892

49-
const src = getGoogleFontURL([{ family, variants }]);
93+
return document.fonts
94+
.load(fontFaceSet, content || '')
95+
.then(() => document.fonts.check(fontFaceSet, content || ''));
96+
};
5097

51-
const fontStylesheet = document.createElement('link');
52-
fontStylesheet.id = id;
53-
fontStylesheet.href = src;
54-
fontStylesheet.rel = 'stylesheet';
55-
fontStylesheet.type = 'text/css';
56-
fontStylesheet.media = 'all';
57-
fontStylesheet.crossOrigin = 'anonymous';
98+
if (!hasFontLink()) {
99+
await appendFontLink();
100+
}
58101

59-
document.head.appendChild(fontStylesheet);
102+
return ensureFontLoaded();
103+
}
104+
)
105+
.map(reflect)
106+
);
60107
}, []);
61108

62109
return maybeEnqueueFontStyle;
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright 2020 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/**
18+
* External dependencies
19+
*/
20+
import { renderHook } from '@testing-library/react-hooks';
21+
22+
/**
23+
* Internal dependencies
24+
*/
25+
import useLoadFontFiles from '../../actions/useLoadFontFiles';
26+
27+
const DEFAULT_FONT = {
28+
font: {
29+
family: 'Font',
30+
service: 'fonts.google.com',
31+
},
32+
fontWeight: 400,
33+
fontStyle: 'normal',
34+
content: 'Fill in some text',
35+
};
36+
37+
describe('useLoadFontFiles', () => {
38+
beforeEach(() => {
39+
const el = document.getElementById('font-css');
40+
if (el) {
41+
el.remove();
42+
}
43+
});
44+
45+
it('maybeEnqueueFontStyle', () => {
46+
expect(document.getElementById('font-css')).toBeNull();
47+
48+
renderHook(async () => {
49+
const maybeEnqueueFontStyle = useLoadFontFiles();
50+
51+
await maybeEnqueueFontStyle([DEFAULT_FONT]);
52+
});
53+
54+
expect(document.getElementById('font-css')).toBeDefined();
55+
});
56+
57+
it('maybeEnqueueFontStyle skip', () => {
58+
expect(document.getElementById('font-css')).toBeNull();
59+
60+
renderHook(async () => {
61+
const maybeEnqueueFontStyle = useLoadFontFiles();
62+
63+
await maybeEnqueueFontStyle([
64+
{ ...DEFAULT_FONT, font: { ...DEFAULT_FONT.font, service: 'abcd' } },
65+
]);
66+
});
67+
68+
expect(document.getElementById('font-css')).toBeNull();
69+
});
70+
71+
it('maybeEnqueueFontStyle reflect', () => {
72+
expect(document.getElementById('font-css')).toBeNull();
73+
74+
renderHook(async () => {
75+
const maybeEnqueueFontStyle = useLoadFontFiles();
76+
77+
await maybeEnqueueFontStyle([{}, DEFAULT_FONT]);
78+
});
79+
80+
expect(document.querySelectorAll('link')).toHaveLength(1);
81+
expect(document.getElementById('font-css')).toBeDefined();
82+
});
83+
});

assets/src/edit-story/components/library/text/fontPreview.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { useEffect } from 'react';
2828
import { useFont } from '../../../app';
2929
import { ALLOWED_EDITOR_PAGE_WIDTHS, PAGE_WIDTH } from '../../../constants';
3030
import { FontPropType } from '../../../types';
31+
import stripHTML from '../../../utils/stripHTML';
3132

3233
const PREVIEW_EM_SCALE = ALLOWED_EDITOR_PAGE_WIDTHS[0] / PAGE_WIDTH;
3334

@@ -53,14 +54,20 @@ const Text = styled.span`
5354
color: ${({ theme }) => theme.colors.fg.v1};
5455
`;
5556

56-
function FontPreview({ title, font, fontSize, fontWeight, onClick }) {
57+
function FontPreview({ title, font, fontSize, fontWeight, content, onClick }) {
5758
const {
5859
actions: { maybeEnqueueFontStyle },
5960
} = useFont();
6061

6162
useEffect(() => {
62-
maybeEnqueueFontStyle(font);
63-
}, [font, maybeEnqueueFontStyle]);
63+
maybeEnqueueFontStyle([
64+
{
65+
font,
66+
fontWeight,
67+
content: stripHTML(content),
68+
},
69+
]);
70+
}, [font, fontWeight, content, maybeEnqueueFontStyle]);
6471

6572
return (
6673
<Preview onClick={onClick}>
@@ -80,6 +87,7 @@ FontPreview.propTypes = {
8087
font: FontPropType,
8188
fontSize: PropTypes.number,
8289
fontWeight: PropTypes.number,
90+
content: PropTypes.string,
8391
onClick: PropTypes.func,
8492
};
8593

assets/src/edit-story/components/panels/test/textStyle.js

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ function Wrapper({ children }) {
7373
],
7474
},
7575
actions: {
76+
maybeEnqueueFontStyle: () => Promise.resolve(),
7677
getFontByName: () => ({
7778
name: 'Neu Font',
7879
value: 'Neu Font',
@@ -420,9 +421,9 @@ describe('Panels/TextStyle', () => {
420421
});
421422

422423
describe('FontControls', () => {
423-
it('should select font', () => {
424+
it('should select font', async () => {
424425
const { pushUpdate } = renderTextStyle([textElement]);
425-
act(() => controls.font.onChange('Neu Font'));
426+
await act(() => controls.font.onChange('Neu Font'));
426427
expect(pushUpdate).toHaveBeenCalledWith(
427428
{
428429
font: {
@@ -441,9 +442,9 @@ describe('Panels/TextStyle', () => {
441442
);
442443
});
443444

444-
it('should select font weight', () => {
445+
it('should select font weight', async () => {
445446
const { pushUpdate } = renderTextStyle([textElement]);
446-
act(() => controls['font.weight'].onChange('300'));
447+
await act(() => controls['font.weight'].onChange('300'));
447448
const updatingFunction = pushUpdate.mock.calls[0][0];
448449
const resultOfUpdating = updatingFunction({ content: 'Hello world' });
449450
expect(resultOfUpdating).toStrictEqual(
@@ -454,17 +455,17 @@ describe('Panels/TextStyle', () => {
454455
);
455456
});
456457

457-
it('should select font size', () => {
458+
it('should select font size', async () => {
458459
const { getByTestId, pushUpdate } = renderTextStyle([textElement]);
459460
const input = getByTestId('font.size');
460-
fireEvent.change(input, { target: { value: '32' } });
461+
await fireEvent.change(input, { target: { value: '32' } });
461462
expect(pushUpdate).toHaveBeenCalledWith({ fontSize: 32 });
462463
});
463464

464-
it('should select font size to empty value', () => {
465+
it('should select font size to empty value', async () => {
465466
const { getByTestId, pushUpdate } = renderTextStyle([textElement]);
466467
const input = getByTestId('font.size');
467-
fireEvent.change(input, { target: { value: '' } });
468+
await fireEvent.change(input, { target: { value: '' } });
468469
expect(pushUpdate).toHaveBeenCalledWith({ fontSize: '' });
469470
});
470471
});

assets/src/edit-story/components/panels/textStyle/font.js

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { PAGE_HEIGHT } from '../../../constants';
3434
import { useFont } from '../../../app/font';
3535
import { getCommonValue } from '../utils';
3636
import objectPick from '../../../utils/objectPick';
37+
import stripHTML from '../../../utils/stripHTML';
3738
import useRichTextFormatting from './useRichTextFormatting';
3839
import getFontWeights from './getFontWeights';
3940

@@ -54,18 +55,19 @@ function FontControls({ selectedElements, pushUpdate }) {
5455
const fontSize = getCommonValue(selectedElements, 'fontSize');
5556

5657
const {
57-
textInfo: { fontWeight },
58+
textInfo: { fontWeight, isItalic },
5859
handlers: { handleSelectFontWeight },
5960
} = useRichTextFormatting(selectedElements, pushUpdate);
6061

6162
const {
6263
state: { fonts },
63-
actions: { getFontByName },
64+
actions: { maybeEnqueueFontStyle, getFontByName },
6465
} = useFont();
6566
const fontWeights = useMemo(() => getFontWeights(getFontByName(fontFamily)), [
6667
getFontByName,
6768
fontFamily,
6869
]);
70+
const fontStyle = isItalic ? 'italic' : 'normal';
6971

7072
return (
7173
<>
@@ -76,21 +78,33 @@ function FontControls({ selectedElements, pushUpdate }) {
7678
ariaLabel={__('Font family', 'web-stories')}
7779
options={fonts}
7880
value={fontFamily}
79-
onChange={(value) => {
81+
onChange={async (value) => {
8082
const fontObj = fonts.find((item) => item.value === value);
83+
const newFont = {
84+
family: value,
85+
...objectPick(fontObj, [
86+
'service',
87+
'fallbacks',
88+
'weights',
89+
'styles',
90+
'variants',
91+
]),
92+
};
93+
94+
await maybeEnqueueFontStyle(
95+
selectedElements.map(({ content }) => {
96+
return {
97+
font: newFont,
98+
fontStyle,
99+
fontWeight,
100+
content: stripHTML(content),
101+
};
102+
})
103+
);
81104

82105
pushUpdate(
83106
{
84-
font: {
85-
family: value,
86-
...objectPick(fontObj, [
87-
'service',
88-
'fallbacks',
89-
'weights',
90-
'styles',
91-
'variants',
92-
]),
93-
},
107+
font: newFont,
94108
},
95109
true
96110
);
@@ -107,7 +121,19 @@ function FontControls({ selectedElements, pushUpdate }) {
107121
placeholder={__('(multiple)', 'web-stories')}
108122
options={fontWeights}
109123
value={fontWeight}
110-
onChange={handleSelectFontWeight}
124+
onChange={async (value) => {
125+
await maybeEnqueueFontStyle(
126+
selectedElements.map(({ font, content }) => {
127+
return {
128+
font,
129+
fontStyle,
130+
fontWeight: parseInt(value),
131+
content: stripHTML(content),
132+
};
133+
})
134+
);
135+
handleSelectFontWeight(value);
136+
}}
111137
/>
112138
<Space />
113139
</>

0 commit comments

Comments
 (0)